多线程编程指南

同步线程

共享数据和进程资源时,应用程序中的线程必须彼此协作并进行同步。

多个线程调用处理同一对象的函数时,会引发问题。在单线程环境中,同步对这类对象的访问不是问题。但是,如示例 9–3 所示,同步对于多线程代码是个问题。请注意,对于多线程程序,可以安全调用 printf(3S) 函数。本示例说明当 printf() 不安全时可能会发生的情况。


示例 9–3 printf() 问题

/* thread 1: */

    printf("go to statement reached");





/* thread 2: */

    printf("hello world");







printed on display:

    go to hello

单线程策略

一个策略是,只要应用程序中的任何线程处于运行状态并在必须阻塞之前被释放,即可获取单个应用程序范围的互斥锁。由于无论何时都只能有一个线程可以访问共享数据,因此每个线程都有一致的内存视图。

由于此策略仅对单线程非常有效,因此此策略的使用范围非常小。

可重复执行函数

更好的方法是利用模块化和数据封装的原理。可重复执行函数可以在同时被多个线程调用的情况下正确执行。要编写可重复执行函数,需要大致了解正确操作对此特定函数的意义。

必须使被多个线程调用的函数可重复执行。要使函数可重复执行,可能需要对函数接口或实现进行更改。

访问全局状态(如内存或文件)的函数具有可重复执行问题。这些函数需要借助线程提供的相应同步机制来保护其全局状态的使用。

使模块中的函数可重复执行的两个基本策略是代码锁定和数据锁定。

代码锁定

代码锁定是在函数调用级别执行的,而且可保证函数在锁定保护下完全执行。该假设针对通过函数对数据执行的所有访问。共享数据的函数应该在同一锁定下执行。

某些并行编程语言提供一种构造,称为监视程序。监视程序可以对监视程序范围内定义的函数隐式执行代码锁定。还可以通过互斥锁来实现监视。

可保证受同一互斥锁保护或同一监视程序中的函数相对于监视程序中的其他函数以原子方式执行。

数据锁定

数据锁定可保证一直维护对数据集合的访问。对于数据锁定,代码锁定概念仍然存在,但代码锁定只是对共享(全局)数据的轮流引用。对于互斥锁,在每个数据集合的临界段中只能有一个线程。

另外,在多个读取器、单个写入器协议中,允许每个数据集合或一个写入器具有多个读取器。当多个线程对不同数据集合执行操作时,这些线程可以在单个模块中执行。需要特别指出的是,对于多个读取器、单个写入器协议,这些线程不会在单个集合上发生冲突。因此,数据锁定通常比代码锁定具备的并发性更多。

使用锁定时应使用哪种策略,在程序中实现互斥、条件变量还是信号?是要尝试通过仅在必要时锁定并在不必要时尽快解除锁定来实现最大并行性(这种方法称作“细粒度锁定 (fine-grained locking)”)?还是要长期持有锁定,以使使用和释放锁的开销降到最低程度(这种方法称作“粗粒度锁定 (coarse-grained locking)”)?

锁定的粒度取决于锁定所保护的数据量。粒度非常粗的锁定可能是单一锁定,目的是保护所有数据。划分由适当数目的锁定保护数据的方式非常重要。锁定粒度过细可能会降低性能。当应用程序包含太多锁定时,与获取和释放锁关联的开销可能会变得非常大。

常见的明智之举是先使用粗粒度方法,确定瓶颈,并在必要时添加细粒度锁定来缓解瓶颈。此方法听起来是很合理的建议,但是您应该自行判断如何在最大化并行性与最小化锁定开销之间找到平衡。

不变量和锁定

对于代码锁定和数据锁定,不变量对于控制锁定复杂性非常重要。不变量指始终为真的条件或关系。

对于并发执行,其定义修改如下(在上述定义的基础上稍加修改即可得到此定义):不变量是在设置关联锁定时为真的条件或关系。设置锁定后,不变量可能为假。但是,在释放锁之前,持有锁的代码必须重新建立不变量。

不变量还可以是设置锁定时为真的条件或关系。条件变量可以被认为含有一个不变量,而这个不变量就是这个条件。


示例 9–4 使用 assert(3X) 测试不变量

    mutex_lock(&lock);

    while((condition)==FALSE)

        cond_wait(&cv,&lock);

    assert((condition)==TRUE);

      .

      .

      .

    mutex_unlock(&lock);

assert() 语句用于测试不变量。cond_wait() 函数不保留不变量,这就是在线程返回时必须重新评估不变量的原因所在。

另一个示例就是用于管理双重链接的元素列表的模块。对于该列表中的每一项,良好的不变量是列表中前一项的向前指针。向前指针还应与向前项的向后指针指向同一元素。

假设此模块使用基于代码的锁定,进而受到单个全局互斥锁的保护。删除或添加项时,将获取互斥锁,正确处理指针,而且会释放互斥锁。显然,在处理指针的某一时间点,不变量为假,但在释放互斥锁之前,需要重新建立不变量。