第 3 部分
内容:
条件变量
pthread_cond_wait() 小测验
初始化和清除
等待
发送信号和广播
工作组
队列
data_control 代码
调试时间
工作组代码
代码初排
有关清除的注重事项
创建工作
threadfunc()
join_threads()
结束语
参考资料
关于作者
相关内容:
第 2 部分:称作互斥对象的小玩意
第 1 部分:POSIX 线程详解
使用条件变量提高效率
作者:Daniel Robbins
本文是 POSIX 线程三部曲系列的最后一部分,Daniel 将具体讨论如何使用条件变量。条件变量是 POSIX 线程结构,可以让您在碰到某些条件时“唤醒”线程。可以将它们看作是一种线程安全的信号发送。Daniel 使用目前您所学到的知识实现了一个多线程工作组应用程序,本文将围绕着这一示例而进行讨论。
条件变量详解
在上一篇文章结束时,我描述了一个比较非凡的难题:假如线程正在等待某个特定条件发生,它应该如何处理这种情况?它可以重复对互斥对象锁定和解锁,每次都会检查共享数据结构,以查找某个值。但这是在浪费时间和资源,而且这种繁忙查询的效率非常低。解决这个问题的最佳方法是使用 pthread_cond_wait() 调用来等待非凡条件发生。
了解 pthread_cond_wait() 的作用非常重要 -- 它是 POSIX 线程信号发送系统的核心,也是最难以理解的部分。
首先,让我们考虑以下情况:线程为查看已链接列表而锁定了互斥对象,然而该列表恰巧是空的。这一特定线程什么也干不了 -- 其设计意图是从列表中除去节点,但是现在却没有节点。因此,它只能:
锁定互斥对象时,线程将调用 pthread_cond_wait(&mycond,&mymutex)。pthread_cond_wait() 调用相当复杂,因此我们每次只执行它的一个操作。
pthread_cond_wait() 所做的第一件事就是同时对互斥对象解锁(于是其它线程可以修改已链接列表),并等待条件 mycond 发生(这样当 pthread_cond_wait() 接收到另一个线程的“信号”时,它将清醒)。现在互斥对象已被解锁,其它线程可以访问和修改已链接列表,可能还会添加项。
此时,pthread_cond_wait() 调用还未返回。对互斥对象解锁会立即发生,但等待条件 mycond 通常是一个阻塞操作,这意味着线程将睡眠,在它清醒之前不会消耗 CPU 周期。这正是我们期待发生的情况。线程将一直睡眠,直到特定条件发生,在这期间不会发生任何浪费 CPU 时间的繁忙查询。从线程的角度来看,它只是在等待 pthread_cond_wait() 调用返回。
现在继续说明,假设另一个线程(称作“2 号线程”)锁定了 mymutex 并对已链接列表添加了一项。在对互斥对象解锁之后,2 号线程会立即调用函数 pthread_cond_broadcast(&mycond)。此操作之后,2 号线程将使所有等待 mycond 条件变量的线程立即清醒。这意味着第一个线程(仍处于 pthread_cond_wait() 调用中)现在将清醒。
现在,看一下第一个线程发生了什么。您可能会认为在 2 号线程调用 pthread_cond_broadcast(&mymutex) 之后,1 号线程的 pthread_cond_wait() 会立即返回。不是那样!实际上,pthread_cond_wait() 将执行最后一个操作:重新锁定 mymutex。一旦 pthread_cond_wait() 锁定了互斥对象,那么它将返回并答应 1 号线程继续执行。那时,它可以马上检查列表,查看它所感爱好的更改。
停止并回顾!
那个过程非常复杂,因此让我们先往返顾一下。第一个线程首先调用:
pthread_mutex_lock(&mymutex);
然后,它检查了列表。没有找到感爱好的东西,于是它调用:
pthread_cond_wait(&mycond, &mymutex);
然后,pthread_cond_wait() 调用在返回前执行许多操作:
pthread_mutex_unlock(&mymutex);
它对 mymutex 解锁,然后进入睡眠状态,等待 mycond 以接收 POSIX 线程“信号”。一旦接收到“信号”(加引号是因为我们并不是在讨论传统的 UNIX 信号,而是来自 pthread_cond_signal() 或 pthread_cond_broadcast() 调用的信号),它就会清醒。但 pthread_cond_wait() 没有立即返回 -- 它还要做一件事:重新锁定 mutex:
pthread_mutex_lock(&mymutex);
pthread_cond_wait() 知道我们在查找 mymutex “背后”的变化,因此它继续操作,为我们锁定互斥对象,然后才返回。
pthread_cond_wait() 小测验
现在已回顾了 pthread_cond_wait() 调用,您应该了解了它的工作方式。应该能够叙述 pthread_cond_wait() 依次执行的所有操作。尝试一下。假如理解了 pthread_cond_wait(),其余部分就相当轻易,因此请重新阅读以上部分,直到记住为止。好,读完之后,能否告诉我在调用 pthread_cond_wait() 之前,互斥对象必须处于什么状态?pthread_cond_wait() 调用返回之后,互斥对象处于什么状态?这两个问题的答案都是“锁定”。既然已经完全理解了 pthread_cond_wait() 调用,现在来继续研究更简单的东西 -- 初始化和真正的发送信号和广播进程。到那时,我们将会对包含了多线程工作队列的 C 代码了如指掌。
初始化和清除
条件变量是一个需要初始化的真实数据结构。以下就初始化的方法。首先,定义或分配一个条件变量,如下所示:
pthread_cond_t mycond;
然后,调用以下函数进行初始化:
pthread_cond_init(&mycond,NULL);
瞧,初始化完成了!在释放或废弃条件变量之前,需要毁坏它,如下所示:
pthread_cond_destroy(&mycond);
很简单吧。接着讨论 pthread_cond_wait() 调用。
等待
一旦初始化了互斥对象和条件变量,就可以等待某个条件,如下所示:
pthread_cond_wait(&mycond, &mymutex);
请注重,代码在逻辑上应该包含 mycond 和 mymutex。一个特定条件只能有一个互斥对象,而且条件变量应该表示互斥数据“内部”的一种非凡的条件更改。一个互斥对象可以用许多条件变量(例如,cond_empty、cond_full、cond_cleanup),但每个条件变量只能有一个互斥对象。
发送信号和广播
对于发送信号和广播,需要注重一点。假如线程更改某些共享数据,而且它想要唤醒所有正在等待的线程,则应使用 pthread_cond_broadcast 调用,如下所示:
pthread_cond_broadcast(&mycond);
在某些情况下,活动线程只需要唤醒第一个正在睡眠的线程。假设您只对队列添加了一个工作作业。那么只需要唤醒一个工作程序线程(再唤醒其它线程是不礼貌的!):
pthread_cond_signal(&mycond);
此函数只唤醒一个线程。假如 POSIX 线程标准答应指定一个整数,可以让您唤醒一定数量的正在睡眠的线程,那就更完美了。但是很可惜,我没有被邀请参加会议。
工作组
我将演示如何创建多线程工作组。在这个方案中,我们创建了许多工作程序线程。每个线程都会检查 wq(“工作队列”),查看是否有需要完成的工作。假如有需要完成的工作,那么线程将从队列中除去一个节点,执行这些特定工作,然后等待新的工作到达。
与此同时,主线程负责创建这些工作程序线程、将工作添加到队列,然后在它退出时收集所有工作程序线程。您将会碰到许多 C 代码,好好预备吧!
队列
需要队列是出于两个原因。首先,需要队列来保存工作作业。还需要可用于跟踪已终止线程的数据结构。还记得前几篇文章(请参阅本文结尾处的参考资料)中,我曾提到过需要使用带有特定进程标识的 pthread_join 吗?使用“清除队列”(称作 "cq")可以解决无法等待任何已终止线程的问题(稍后将具体讨论这个问题)。以下是标准队列代码。将此代码保存到文件 queue.h 和 queue.c:
queue.h
/* queue.h
** Copyright 2000 Daniel Robbins, Gentoo Technologies, Inc.
** Author: Daniel Robbins
** Date: 16 Jun 2000
*/
typedef strUCt node {
struct node *next;
} node;