资讯动态

TLPI 第30 章 读书笔记:Threads: Thread Synchronization

发布时间:2026/8/12 18:59:00 来源:尧图企业网站定制
笔记和练习博客总目录见开始读TLPI。在本章中我们介绍了线程可以用来同步它们操作的两种工具互斥锁和条件变量。互斥锁让线程可以同步使用共享资源这样例如一个线程就不会在另一个线程修改共享变量时同时去访问它。条件变量则做一个互补的工作它们允许线程互相通知某个共享变量或其他共享资源的状态已经发生变化。30.1 Protecting Accesses to Shared Variables: Mute.„线程的一个主要优点是它们可以通过全局变量共享信息。然而这种轻松的共享是有代价的我们必须小心确保多个线程不会同时尝试修改同一个变量或者一个线程在另一个线程修改变量时尝试读取其值。术语“临界区”用来指访问共享资源的一段代码这段代码的执行应该是原子的也就是说其执行不应该被同时访问相同共享资源的另一个线程打断。清单30-1提供了一个简单的例子说明当共享资源未被原子访问时可能出现的问题。该程序创建了两个线程每个线程执行相同的函数。函数执行一个循环重复将全局变量 glob 加 1 的操作先将 glob 复制到局部变量 loc然后将 loc 自增最后再将 loc 复制回 glob。由于 loc 是分配在每个线程栈上的自动变量每个线程都有自己的 loc 副本。循环的迭代次数由程序提供的命令行参数决定如果没有提供参数则使用默认值。Listing 30-1: Incorrectly incrementing a global variable from two threads代码略。Figure 30-1: Two threads incrementing a global variable without synchronization 上图的表示方式值得学习。当我们运行清单 30-1 中的程序并指定每个线程将变量递增 1000 次时一切看起来都很正常$ ./thread_incr1000glob2000然而这里很可能发生的情况是第一个线程完成了它的所有工作并在第二个线程甚至还没开始之前就终止了。当我们让两个线程做更多的工作时我们会看到一个相当不同的结果$ ./thread_incr10000000glob10939877在这个序列结束时glob 的值应该是 2000 万。这里的问题出现在如下执行顺序中也可参见上面的图 30-1线程 1 将当前 glob 的值取到它的本地变量 loc 中。假设当前 glob 的值是 2000。线程 1 的时间片用完线程 2 开始执行。线程 2 执行多个循环每次都把当前 glob 的值取到本地变量 loc增加 loc 的值然后将结果赋给 glob。在这些循环的第一次中从 glob 获取的值将是 2000。假设在线程 2 的时间片用完之前glob 已经被增加到 3000。线程 1 再次获得一个时间片从它中断的地方继续执行。之前步骤 1它已经把 glob 的值2000复制到 loc 中现在它增加 loc 的值并将结果2001赋给 glob。此时线程 2 执行的增加操作的效果就丢失了。如果我们用相同的命令行参数多次运行清单 30-1 中的程序会看到 glob 打印的值会大幅波动$ ./thread_incr10000000glob11413748$ ./thread_incr10000000glob12065910$ ./thread_incr10000000glob11500623$ ./thread_incr10000000glob11338848这种非确定性行为是内核 CPU 调度决策变化无常的结果。在复杂程序中这种非确定性行为意味着这类错误可能很少发生、难以重现因此也很难发现。看起来我们似乎可以通过把 Listing 30-1 中 threadFunc() 函数的 for 循环里的三条语句替换为一条语句来消除这个问题glob;/* or: glob; */然而在许多硬件架构例如 RISC 架构上编译器仍然需要将这条单独的语句转换为与 threadFunc() 循环中的三条语句等效的机器代码。换句话说尽管它看起来很简单即使是 C 语言中的自增操作符也可能不是原子操作而且它可能表现出我们上面描述的行为。 修改源代码为glob后结果确实如以上所述。glob的值普遍大些说明冲突少些了但不能避免冲突$ ./thread_incr10000000glob15562912$ ./thread_incr10000000glob12403784$ ./thread_incr10000000glob12637472$ ./thread_incr10000000glob15818286为了避免线程在尝试更新共享变量时可能出现的问题我们必须使用互斥锁mutex互斥的缩写来确保一次只有一个线程可以访问该变量。更广泛地说互斥锁可以用来保证对任何共享资源的原子访问但保护共享变量是最常见的用途。互斥锁有两种状态锁定和未锁定。在任何时候最多只有一个线程可以持有互斥锁。尝试锁定已被锁定的互斥锁时可能会阻塞或返回错误这取决于用于加锁的方法。当一个线程锁住一个互斥量时它就成为这个互斥量的所有者。只有互斥量的所有者才能解锁它。这个特性改善了使用互斥量的代码结构同时也允许在互斥量实现上进行一些优化。正因为有这个所有权特性术语 acquire获取和 release释放有时会被作为 lock锁定和 unlock解锁的同义词使用。通常我们为每个共享资源可能由多个相关变量组成使用不同的互斥量每个线程在访问资源时遵循以下协议锁住共享资源的互斥量访问共享资源以及解锁互斥量。最后需要注意的是互斥锁的使用是建议性的而不是强制性的。这意味着线程可以选择忽略互斥锁直接访问相应的共享变量。为了安全地处理共享变量所有线程都必须在使用互斥锁时相互配合遵守它所执行的锁定规则。Figure 30-2: Using a mutex to protect a critical section30.1.1 Statically Allocated Mutexes互斥锁可以作为静态变量分配也可以在运行时动态创建例如在通过 malloc() 分配的内存块中。动态创建互斥锁稍微复杂一些我们会把这部分内容留到第 30.1.5 节再讨论。互斥锁是 pthread_mutex_t 类型的变量。在使用之前互斥锁必须先初始化。对于静态分配的互斥锁我们可以通过将其赋值为 PTHREAD_MUTEX_INITIALIZER 来初始化就像下面的示例一样pthread_mutex_tmtxPTHREAD_MUTEX_INITIALIZER;根据 SUSv3对本节剩余部分描述的操作应用到互斥锁的副本上会产生未定义的结果。互斥锁操作应该始终只在通过 PTHREAD_MUTEX_INITIALIZER 静态初始化或使用 pthread_mutex_init()在第 30.1.5 节中描述动态初始化的原始互斥锁上进行。30.1.2 Locking and Unlocking a Mutex初始化后互斥锁是解锁的。要锁定和解锁互斥锁我们使用 pthread_mutex_lock() 和 pthread_mutex_unlock() 函数。#includepthread.hintpthread_mutex_lock(pthread_mutex_t*mutex);intpthread_mutex_unlock(pthread_mutex_t*mutex);Bothreturn0on success,or a positive error number on error要锁定一个互斥量我们在调用 pthread_mutex_lock() 时指定这个互斥量。如果这个互斥量当前是未锁定状态这个调用会立即锁定互斥量并返回。如果互斥量当前被其他线程锁定那么 pthread_mutex_lock() 会阻塞直到互斥量被解锁此时它会锁定互斥量并返回。如果调用线程本身已经锁定了传给 pthread_mutex_lock() 的互斥量那么对于默认类型的互斥量可能会出现两种实现定义的情况线程会死锁试图锁定它已经拥有的互斥量而被阻塞或者调用失败返回错误 EDEADLK。在 Linux 上线程默认会死锁。在 30.1.7 节讨论互斥量类型时我们会描述其他可能的行为。pthread_mutex_unlock() 函数会解锁先前由调用线程锁定的互斥量。解锁一个当前未锁定的互斥量或者解锁一个被其他线程锁定的互斥量都是错误的。如果有多个其他线程在等待获取通过 pthread_mutex_unlock() 解锁的互斥量那么哪个线程能成功获取该互斥量是无法确定的。Example program清单30-2是清单30-1程序的一个修改版本。它使用互斥锁来保护对全局变量glob的访问。当我们用类似前面使用的命令行运行这个程序时我们会看到glob总是可靠地被递增$ ./thread_incr_mutex10000000glob20000000Listing 30-2: Using a mutex to protect access to a global variable代码略。pthread_mutex_trylock()andpthread_mutex_timedlock()Pthreads API 提供了 pthread_mutex_lock() 函数的两个变体pthread_mutex_trylock() 和 pthread_mutex_timedlock()。可以查看手册页了解这些函数的原型。pthread_mutex_trylock() 函数和 pthread_mutex_lock() 一样只不过如果互斥锁当前已经被锁定pthread_mutex_trylock() 会失败并返回错误 EBUSY。pthread_mutex_timedlock() 函数和 pthread_mutex_lock() 一样只是调用者可以指定一个额外的参数 abstime这会限制线程在等待获取互斥锁时可以休眠的时间。如果指定的 abstime 时间间隔到期而调用者仍未成为互斥锁的所有者pthread_mutex_timedlock() 会返回错误 ETIMEDOUT。pthread_mutex_trylock() 和 pthread_mutex_timedlock() 函数的使用频率远低于 pthread_mutex_lock()。在大多数设计良好的应用程序中线程只应该持有互斥锁很短的时间以便其他线程可以并行执行。这保证了那些被互斥锁阻塞的线程很快就能获得互斥锁。使用 pthread_mutex_trylock() 定期轮询互斥锁的线程可能会在其他排队线程通过 pthread_mutex_lock() 依次获取互斥锁的情况下被长期阻塞而无法访问互斥锁。30.1.3 Performance of Mutexes使用互斥锁的代价是多少我们展示了两个不同版本的程序用于增加一个共享变量一个没有互斥锁清单 30-1另一个带有互斥锁清单 30-2。当我们在运行 Linux 2.6.31带 NPTL的 x86-32 系统上运行这两个程序时我们发现没有互斥锁的版本每个线程执行 1000 万次循环总共只需 0.35 秒但结果是错误的而带互斥锁的版本则需要 3.1 秒。起初这看起来很昂贵。但考虑一下不使用互斥锁的版本清单30-1所执行的主循环。在那个版本中threadFunc() 函数执行一个 for 循环对循环控制变量进行递增将该变量与另一个变量进行比较进行两次赋值和另一次递增操作然后跳回循环顶部。使用互斥锁的版本清单30-2执行相同的步骤并且每次循环都会锁定和解锁互斥锁。换句话说锁定和解锁互斥锁的成本大约是我们列出的第一个程序操作成本的十倍以下。这相对来说还是比较便宜的。此外在典型情况下线程会花更多时间去做其他工作执行的互斥锁锁定和解锁操作相对较少因此在大多数应用中使用互斥锁对性能的影响并不大。 虽然开销增长了10倍但保证了正确性这才是最重要的。为了进一步说明这个情况在同一个系统上运行一些简单的测试程序显示使用 fcntl()第 55.3 节对文件区域进行 2000 万次加锁和解锁循环需要 44 秒而对 System V 信号量第 47 章进行 2000 万次加一减一的循环需要 28 秒。文件锁和信号量的问题在于它们每次加锁和解锁操作都需要系统调用而每次系统调用都有一个小但显著的开销第 3.1 节。相比之下互斥锁是使用原子机器语言操作来实现的在所有线程可见的内存位置上进行只有在锁争用的情况下才需要系统调用。在 Linux 上互斥锁是用 futex意为快速用户态互斥锁的缩写实现的锁竞争则通过 futex() 系统调用来处理。我们在本书中没有描述 futex它们并不打算直接在用户空间应用中使用但可以在 [Drepper, 2004 (a)] 中找到相关细节该文也说明了互斥锁是如何用 futex 实现的。[Franke 等, 2002] 是一篇现在已经过时的论文由 futex 的开发者写的描述了早期的 futex 实现并分析了 futex 带来的性能提升。30.1.4 Mutex Deadlocks有时候一个线程需要同时访问两个或更多不同的共享资源而每个资源都有自己独立的互斥锁。当多个线程试图锁定同一组互斥锁时就可能发生死锁。图30-3展示了一个死锁的例子每个线程都成功锁定了一个互斥锁然后尝试锁定另一个线程已经锁定的互斥锁。这两个线程就会一直被阻塞下去。时间线Thread AThread B1pthread_mutex_lock(mutex1);pthread_mutex_lock(mutex2);2pthread_mutex_lock(mutex2);pthread_mutex_lock(mutex1);3blocksblocksFigure 30-3: A deadlock when two threads lock two mutexes避免这种死锁的最简单方法是定义一个互斥锁层次。当线程可能锁定同一组互斥锁时它们应该总是按照相同的顺序锁定它们。例如在图30-3的场景中如果两个线程总是按照先锁mutex1再锁mutex2的顺序锁定互斥锁就可以避免死锁。有时候互斥锁之间存在一个逻辑上显而易见的层次。然而即使没有也可能制定一个任意的层次顺序让所有线程都遵循这个顺序。另一种不太常用的策略是“尝试然后回退”。在这种策略中线程先使用pthread_mutex_lock()锁定第一个互斥锁然后使用pthread_mutex_trylock()锁定剩下的互斥锁。如果任何一个pthread_mutex_trylock()调用失败返回EBUSY线程就释放所有互斥锁然后再尝试可能会有一个延迟间隔。这种方法比锁层次的效率低因为可能需要多次迭代。另一方面它更灵活因为不需要严格的互斥锁层次。这种策略的一个例子见[Butenhof, 1996]。30.1.5 Dynamically Initializing a Mutex静态初始化器值 PTHREAD_MUTEX_INITIALIZER 只能用于初始化具有默认属性的静态分配互斥锁。在其他情况下我们必须使用 pthread_mutex_init() 动态初始化互斥锁。#includepthread.hintpthread_mutex_init(pthread_mutex_t*mutex,constpthread_mutexattr_t*attr);Returns0on success,or a positive error number on errormutex 参数用来指定要初始化的互斥锁。attr 参数是一个指向已经初始化过的 pthread_mutexattr_t 对象的指针用来定义互斥锁的属性。接下来的部分我们会多说一些关于互斥锁属性的内容。如果 attr 指定为 NULL那么互斥锁会使用各种默认属性。SUSv3 指出初始化一个已经初始化过的互斥锁会导致未定义行为我们不应该这样做。有些情况下必须使用 pthread_mutex_init() 而不是静态初始化器包括以下情况互斥锁是在堆上动态分配的。例如假设我们创建了一个动态分配的结构体链表并且链表中的每个结构体都包含一个 pthread_mutex_t 字段用来保护对该结构体的访问。互斥锁是分配在栈上的自动变量。我们想用非默认属性来初始化一个静态分配的互斥锁。当一个自动或动态分配的互斥锁不再需要时应该使用 pthread_mutex_destroy() 来销毁它。对于使用 PTHREAD_MUTEX_INITIALIZER 静态初始化的互斥锁不需要调用 pthread_mutex_destroy()。#includepthread.hintpthread_mutex_destroy(pthread_mutex_t*mutex);Returns0on success,or a positive error number on error只有当互斥锁是解锁状态并且之后没有线程会尝试去锁它时销毁互斥锁才是安全的。如果互斥锁位于动态分配的内存区域中那么在释放那块内存之前应该先销毁它。自动分配的互斥锁应该在它所在的函数返回之前销毁。用 pthread_mutex_destroy() 销毁的互斥锁之后可以用 pthread_mutex_init() 重新初始化。30.1.6 Mutex Attributes如前所述pthread_mutex_init() 的 attr 参数可以用来指定一个 pthread_mutexattr_t 对象该对象定义了互斥锁的属性。可以使用各种 Pthreads 函数来初始化和获取 pthread_mutexattr_t 对象中的属性。我们不会详细介绍互斥锁属性的所有细节也不会展示用于初始化 pthread_mutexattr_t 对象属性的各种函数原型。不过我们会描述可以为互斥锁设置的其中一个属性它的类型。30.1.7 Mutex Types在前面的几页中我们对互斥锁的行为做了一些说明单个线程不能对同一个互斥锁进行两次加锁。线程不能解锁自己当前没有拥有的互斥锁也就是说没有加锁的互斥锁。线程不能解锁当前没有被锁定的互斥锁。在每种情况下具体发生什么取决于互斥锁的类型。SUSv3 定义了以下几种互斥锁类型PTHREAD_MUTEX_NORMAL这种类型的互斥锁不提供自我死锁检测。如果一个线程尝试锁定已经被自己锁定的互斥锁就会导致死锁。解锁一个未锁定或者被其他线程锁定的互斥锁会产生未定义结果。在 Linux 上这两种操作都会成功。PTHREAD_MUTEX_ERRORCHECK对所有操作都进行错误检查。上述三种情况都会导致相关的 Pthreads 函数返回错误。这种类型的互斥锁通常比普通互斥锁慢但作为调试工具很有用可以帮助发现应用程序违反互斥锁使用规则的地方。PTHREAD_MUTEX_RECURSIVE递归互斥锁维持一个锁计数概念。当线程第一次获得互斥锁时锁计数被设置为 1。同一线程的每一次后续锁定操作会增加锁计数每一次解锁操作会减少计数。只有当锁计数降到 0 时互斥锁才会被释放即其他线程可以获取。解锁一个未锁定的互斥锁会失败解锁一个当前被其他线程锁定的互斥锁同样会失败。Linux 的线程实现为上述每种互斥锁类型提供了非标准的静态初始化器例如PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP因此在静态分配的互斥锁上不必使用 pthread_mutex_init() 来初始化这些互斥锁类型。不过可移植的应用程序应该尽量避免使用这些初始化器。除了上述互斥锁类型外SUSv3 定义了 PTHREAD_MUTEX_DEFAULT 类型如果我们在使用 PTHREAD_MUTEX_INITIALIZER 或在调用 pthread_mutex_init() 时将 attr 指定为 NULL 时它就是默认的互斥锁类型。在本节开头描述的三种场景中这种互斥锁类型的行为都是刻意未定义的这样可以为高效实现互斥锁提供最大的灵活性。在 Linux 上PTHREAD_MUTEX_DEFAULT 互斥锁的行为类似于 PTHREAD_MUTEX_NORMAL 互斥锁。清单 30-3 中的代码演示了如何设置互斥锁的类型这里示例是创建一个错误检查互斥锁。Listing 30-3: Setting the mutex type代码略。30.2 Signaling Changes of State: Condition Variables互斥锁可以防止多个线程同时访问共享变量。条件变量允许一个线程通知其他线程共享变量或其他共享资源状态的变化并且允许其他线程等待阻塞这种通知。一个不使用条件变量的简单例子用来演示为什么它们很有用。假设我们有一些线程产生一些“结果单元”这些单元会被主线程消费我们使用一个受互斥锁保护的变量 avail 来表示等待被消费的已产生单元的数量staticpthread_mutex_tmtxPTHREAD_MUTEX_INITIALIZER;staticintavail0;本节中显示的代码段可以在本书源码分发包中的文件 threads/prod_no_condvar.c 中找到。在生产者线程中我们可能会有如下代码for(;;){intspthread_mutex_lock(mtx);if(s!0)errExitEN(s,pthread_mutex_lock);while(avail0){/* Consume all available units *//* Do something with produced unit */numConsumed;avail--;printf(T%ld: numConsumed%d\n,(long)(time(NULL)-t),numConsumed);donenumConsumedtotRequired;}spthread_mutex_unlock(mtx);if(s!0)errExitEN(s,pthread_mutex_unlock);if(done)break;/* Perhaps do other work here that does not require mutex lock */}上面的代码可以运行但它浪费了 CPU 时间因为主线程不断循环检查变量 avail 的状态。条件变量可以解决这个问题。它允许一个线程休眠等待直到另一个线程通知发送信号它必须做某事也就是说某种“条件”已经出现等待的线程现在必须做出反应。运行如下其中T表示相当于程序启动的时间并非线程号$ ./prod_no_condvar432T1:numConsumed1T1:numConsumed2T1:numConsumed3T2:numConsumed4T2:numConsumed5T2:numConsumed6T3:numConsumed7T3:numConsumed8T4:numConsumed9条件变量总是与互斥锁一起使用。互斥锁提供对共享变量的互斥访问而条件变量用来通知signal变量状态的改变。这里的“signal”一词与第 20 到 22 章中描述的信号无关它的意思是表示/指示。30.2.1 Statically Allocated Condition Variables像互斥锁一样条件变量可以静态分配或动态分配。我们会把动态分配的条件变量留到第30.2.5节再讨论这里先看看静态分配的条件变量。条件变量的类型是 pthread_cond_t。和互斥锁一样条件变量在使用前必须先初始化。对于静态分配的条件变量可以通过将其赋值为 PTHREAD_COND_INITIALIZER 来完成初始化示例如下pthread_cond_tcondPTHREAD_COND_INITIALIZER;根据 SUSv3对条件变量的副本执行我们在本节余下部分描述的操作会产生未定义的结果。操作应始终只在使用 PTHREAD_COND_INITIALIZER 静态初始化或使用 pthread_cond_init() 动态初始化见第 30.2.5 节的原始条件变量上进行。主要的条件变量操作是 signal 和 wait。signal 操作是通知一个或多个等待线程共享变量的状态已经发生变化。wait 操作是阻塞线程直到收到这样的通知。pthread_cond_signal() 和 pthread_cond_broadcast() 函数都会通知指定的条件变量 cond。pthread_cond_wait() 函数会阻塞线程直到条件变量 cond 被 signal。30.2.2 Signaling and Waiting on Condition Variables主要的条件变量操作是 signal 和 wait。signal 操作是通知一个或多个等待线程共享变量的状态已经发生变化。wait 操作是阻塞线程直到收到这样的通知。pthread_cond_signal() 和 pthread_cond_broadcast() 函数都会通知指定的条件变量 cond。pthread_cond_wait() 函数会阻塞线程直到条件变量 cond 被 signal。#includepthread.hintpthread_cond_signal(pthread_cond_t*cond);intpthread_cond_broadcast(pthread_cond_t*cond);intpthread_cond_wait(pthread_cond_t*cond,pthread_mutex_t*mutex);Allreturn0on success,or a positive error number on errorpthread_cond_signal() 和 pthread_cond_broadcast() 的区别在于如果有多个线程被 pthread_cond_wait() 阻塞会发生什么。使用 pthread_cond_signal() 时我们只保证至少有一个被阻塞的线程会被唤醒而使用 pthread_cond_broadcast() 时所有被阻塞的线程都会被唤醒。使用 pthread_cond_broadcast() 总是能得到正确的结果因为所有线程都应该被编程成能够处理多余和伪唤醒但是 pthread_cond_signal() 可能更高效。不过pthread_cond_signal() 只应该在只需要唤醒一个等待的线程来处理共享变量状态变化时使用并且唤醒哪一个线程无关紧要。这种情况通常适用于所有等待的线程都被设计来执行完全相同的任务。基于这些假设pthread_cond_signal() 可以比 pthread_cond_broadcast() 更高效因为它避免了如下可能性所有等待的线程都被唤醒了。一个线程先被调度执行。这个线程在相关互斥锁的保护下检查共享变量的状态发现还有工作要做。线程完成必要的工作改变共享变量的状态以表示工作已完成然后解锁相关的互斥锁。剩下的每个线程依次锁住互斥锁并检查共享变量的状态。然而由于第一个线程已经做了修改这些线程发现没有工作要做于是解锁互斥锁并回去休眠也就是再次调用 pthread_cond_wait()。相比之下pthread_cond_broadcast() 处理的是等待线程被设计为执行不同任务的情况在这种情况下它们可能有与条件变量相关的不同谓词。条件变量本身不存储任何状态信息。它只是一个用于传递应用程序状态信息的机制。如果在条件变量被发信号时没有线程在等待那么信号就会丢失。之后等待该条件变量的线程只有在变量再次被发信号时才会被唤醒。pthread_cond_timedwait() 函数和 pthread_cond_wait() 一样只不过它的 abstime 参数指定了线程在等待条件变量发信号时最多可以休眠的时间。#includepthread.hintpthread_cond_timedwait(pthread_cond_t*cond,pthread_mutex_t*mutex,conststructtimespec*abstime);Returns0on success,or a positive error number on errorabstime 参数是一个 timespec 结构第 23.4.2 节指定了一个绝对时间以自纪元第 10.1 节起的秒和纳秒表示。如果 abstime 指定的时间间隔到期而条件变量没有被唤醒那么 pthread_cond_timedwait() 会返回错误 ETIMEDOUT。Using a condition variable in the producer-consumer example此略。详见随书代码 threads/prod_condvar.c 。并注意与prod_no_condvar.c的差异。在考虑消费者的代码之前我们需要更详细地解释一下 pthread_cond_wait()。我们之前提到过条件变量总是有一个关联的互斥锁。这两个对象都会作为参数传递给 pthread_cond_wait()它会执行以下步骤解锁 mutex 指定的互斥锁阻塞调用线程直到另一个线程发出条件变量 cond 的信号重新加锁 mutex。pthread_cond_wait() 函数之所以设计成执行这些步骤是因为我们通常以如下方式访问共享变量spthread_mutex_lock(mtx);if(s!0)errExitEN(s,pthread_mutex_lock);while(/* Check that shared variable is not in state we want */)pthread_cond_wait(cond,mtx);/* Now shared variable is in desired state; do some work */spthread_mutex_unlock(mtx);if(s!0)errExitEN(s,pthread_mutex_unlock);我们将在下一节解释为什么 pthread_cond_wait() 调用被放在 while 循环中而不是 if 语句中。在上面的代码中对共享变量的访问都必须用互斥锁保护原因如前所述。换句话说互斥锁和条件变量之间有一种自然的关联线程在准备检查共享变量状态时会先锁住互斥锁。检查共享变量的状态。如果共享变量不在期望的状态线程必须先解锁互斥锁这样其他线程才能访问共享变量然后才会在条件变量上进入休眠。当线程因为条件变量被触发而重新唤醒时必须再次锁住互斥锁因为通常线程会立刻去访问共享变量。pthread_cond_wait() 函数会自动完成这几个步骤中最后两个所需的互斥锁解锁和加锁。在第三步中释放互斥锁和在条件变量上阻塞是原子操作。换句话说在调用 pthread_cond_wait() 的线程在条件变量上阻塞之前其他线程不可能先获取互斥锁并发送条件信号。有一个相关的结论是条件变量和互斥量之间有天然的关系——所有同时等待某个条件变量的线程在调用 pthread_cond_wait()或 pthread_cond_timedwait()时都必须指定同一个互斥量。实际上pthread_cond_wait() 调用在执行期间会动态地将条件变量绑定到唯一的互斥量。SUSv3 指出如果在同一个条件变量上对并发的 pthread_cond_wait() 调用使用多个互斥量其结果是未定义的。我们最后来谈一下关于使用 pthread_cond_signal()以及 pthread_cond_broadcast()的一个观察。在前面展示的生产者代码中我们先调用了 pthread_mutex_unlock()然后再调用 pthread_cond_signal()也就是说我们先释放了与共享变量关联的互斥锁然后再通知对应的条件变量。我们也可以把这两个步骤颠倒过来SUSv3 允许它们按任意顺序执行。[Butenhof, 1996] 指出在某些实现中先解锁互斥锁再发出条件变量信号可能比按相反顺序执行性能更好。如果在发出条件变量信号后才解锁互斥锁那么正在执行 pthread_cond_wait() 的线程可能会在互斥锁仍然被锁住的情况下被唤醒然后发现锁被占用又立即回去睡觉这会造成两次多余的上下文切换。一些实现通过使用所谓的“等待变形wait morphing”技术来解决这个问题该技术会在互斥锁被锁住时将被唤醒的线程从条件变量等待队列直接移动到互斥锁等待队列而不用进行上下文切换。 其实我有一个疑问为何pthread_mutex_lock后面还要加上pthread_cond_wait? pthread_mutex_lock成功不久表示有任务可做了吗为了解除这个疑问我修改了代码增加了一行来看pthread_cond_wait是否有机会被执行for(;;){intspthread_mutex_lock(mtx);if(s!0)errExitEN(s,pthread_mutex_lock);while(avail0){/* Wait for something to consume */// 我增加了下面这行printf(T%ld: pthread_cond_wait is executed!\n,(long)(time(NULL)-t));spthread_cond_wait(cond,mtx);if(s!0)errExitEN(s,pthread_cond_wait);}运行如下pthread_cond_wait确实被执行了$ ./prod_condvar234T0: pthread_cond_wait is executed!T1:numConsumed1T1:numConsumed2T1: pthread_cond_wait is executed!T1:numConsumed3T1: pthread_cond_wait is executed!T2:numConsumed4T2:numConsumed5T2: pthread_cond_wait is executed!T2:numConsumed6T2: pthread_cond_wait is executed!T3:numConsumed7T3:numConsumed8T3: pthread_cond_wait is executed!T4:numConsumed930.2.3 Testing a Condition Variable’s Predicate每个条件变量都有一个与之相关的谓词涉及一个或多个共享变量。例如在前一节的代码片段中与 cond 相关的谓词是 (avail 0)。这个代码片段展示了一个通用的设计原则pthread_cond_wait() 调用必须由 while 循环控制而不是 if 语句。这是因为从 pthread_cond_wait() 返回时无法保证谓词的状态因此如果谓词不在期望状态我们应该立即重新检查谓词并继续等待。从 pthread_cond_wait() 返回时我们不能对谓词的状态做任何假设原因如下其他线程可能会先被唤醒。也许有几个线程在等待获取与条件变量相关的互斥锁。即使发出信号的线程已经将条件谓词设置为期望的状态仍有可能其他线程先获取互斥锁并改变相关共享变量的状态从而改变谓词的状态。为“宽松”谓词进行设计可能更简单。有时候基于条件变量设计应用程序会更容易特别是当条件变量表示可能性而非确定性的时候。换句话说给条件变量发信号意味着“可能有事情”需要被唤醒的线程去处理而不是“肯定有事情”。使用这种方法条件变量可以根据谓词状态的近似值发送信号被唤醒的线程可以通过重新检查谓词来确认是否确实有事情需要做。可能会出现虚假唤醒。在某些实现中一个线程在等待条件变量时即便没有其他线程实际发出信号也可能被唤醒。这类虚假唤醒是某些多处理器系统为了高效实现所需技术的罕见副作用并且被 SUSv3 明确允许。 其实也没有完全理解当作固定套路照做就好30.2.4 Example Program: Joining Any Terminated Thread我们之前提到过pthread_join() 只能用来等待特定的线程结束。它没有机制去等待任何已经终止的线程。现在我们展示如何使用条件变量来绕过这个限制。列表 30-4 中的程序为它的每个命令行参数创建了一个线程。每个线程会休眠与对应命令行参数指定的秒数相同的时间然后结束。休眠时间是我们模拟线程执行一段时间工作的方式。该程序维护了一组全局变量用来记录所有已创建线程的信息。对于每个线程全局线程数组中的一个元素记录该线程的 IDtid 字段以及它的当前状态state 字段。状态字段有以下几种值TS_ALIVE表示线程仍在运行TS_TERMINATED表示线程已结束但尚未被 join或者 TS_JOINED表示线程已结束并且被 join 了。每当一个线程结束时它会将其在线程数组中的元素的 state 字段设置为 TS_TERMINATED同时增加一个全局计数器 numUnjoined记录已终止但尚未 join 的线程数量并唤醒条件变量 threadDied。主线程通过一个循环不断等待条件变量 threadDied。一旦 threadDied 被唤醒并且有已终止但尚未 join 的线程主线程就会扫描线程数组寻找 state 字段为 TS_TERMINATED 的元素。对于每个处于该状态的线程使用线程数组中的对应 tid 字段调用 pthread_join()然后将其状态设置为 TS_JOINED。当主线程创建的所有线程都结束即全局变量 numLive 为 0 时主循环终止。下面的 shell 会话日志演示了程序在清单 30-4 中的使用$ ./thread_multijoin21313Thread1terminating Reaped thread1(numLive4)Thread3terminating Reaped thread3(numLive3)Thread0terminating Reaped thread0(numLive2)Thread4terminating Reaped thread4(numLive1)Thread2terminating Reaped thread2(numLive0)最后请注意虽然示例程序中的线程是以可连接joinable方式创建的并且在线程终止时立即通过 pthread_join() 回收但我们不一定需要这种方式来了解线程的终止情况。我们也可以将线程设为分离detached状态不使用 pthread_join()然后仅使用线程数组以及相关的全局变量来记录每个线程的终止。Listing 30-4: A main thread that can join with any terminated thread// threads/thread_multijoin.c// 代码略。30.2.5 Dynamically Allocated Condition Variablespthread_cond_init() 函数用于动态初始化条件变量。需要使用 pthread_cond_init() 的情况类似于需要 pthread_mutex_init() 动态初始化互斥锁的情况见第 30.1.5 节也就是说我们必须使用 pthread_cond_init() 来初始化自动和动态分配的条件变量以及使用非默认属性来初始化静态分配的条件变量。#includepthread.hintpthread_cond_init(pthread_cond_t*cond,constpthread_condattr_t*attr);Returns0on success,or a positive error number on errorSUSv3 指出初始化一个已经初始化的条件变量会导致未定义行为我们不应该这样做。当一个自动分配或动态分配的条件变量不再需要时应该使用 pthread_cond_destroy() 来销毁它。对于使用 PTHREAD_COND_INITIALIZER 静态初始化的条件变量则不需要调用 pthread_cond_destroy()。#includepthread.hintpthread_cond_destroy(pthread_cond_t*cond);Returns0on success,or a positive error number on error只有当没有线程在等待它时销毁条件变量才是安全的。如果条件变量位于动态分配的内存区域那么在释放该内存区域之前应该先销毁它。自动分配的条件变量应该在它所在的函数返回之前销毁。用 pthread_cond_destroy() 销毁的条件变量之后可以通过 pthread_cond_init() 重新初始化。30.3 Summary线程提供的更大共享是有代价的。多线程应用程序必须使用同步原语比如互斥锁和条件变量以协调对共享变量的访问。互斥锁提供对共享变量的独占访问。条件变量允许一个或多个线程等待通知以得知其他线程已经改变了共享变量的状态。Further information请参考第29.10节列出的更多信息来源。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价