使用条件变量

条件变量允许线程一直等待,直到一些事件或者条件发生。

条件变量具有指定条件特征的属性。 通常,程序使用以下对象:
  • 布尔值(指示是否满足条件)
  • 互斥对象(串行化对布尔变量的访问)
  • 条件变量(等待条件)

使用条件变量需要程序员执行一些操作。 但是,条件变量允许实现强大且高效的同步机制。 有关使用条件变量实现长锁和信号量的更多信息,请参阅 创建复杂同步对象

当线程被终止时,可能无法回收其存储空间,这取决于线程的属性。 这样的线程能与其他线程连接且将信息返回给它们。 要连接其他线程的线程会被阻塞,直到目标线程终止。 此连接机制是条件变量用法的特定情况,条件为线程终止。

Condition 属性对象

类似于线程和互斥对象,条件变量是在属性对象的帮助下创建的。 条件属性对象是抽象对象,其最多包含一个属性,取决于 POSIX 选项的实现。 可通过类型为 pthread_condattr_t的变量进行访问。 在AIX® 中,pthread_condattr_t数据类型是指针;在其他系统中,它可能是结构体或其他数据类型。

创建和破坏条件属性对象

条件属性对象由 pthread_condattr_init 子例程初始化为缺省值。 属性由子例程处理。 线程属性对象被 pthread_condattr_destroy 子例程破坏。 此子例程能够释放 pthread_condattr_init 子例程动态分配的存储空间,这取决于线程库的实现。

在下面的示例中,创建条件属性对象并将其初始化为缺省值,然后使用该对象,最终删除它:
pthread_condattr_t attributes;
                /* the attributes object is created */
...
if (!pthread_condattr_init(&attributes)) {
                /* the attributes object is initialized */
        ...
                /* using the attributes object */
        ...
        pthread_condattr_destroy(&attributes);
                        /* the attributes object is destroyed */
}

同一个属性对象能用来创建多个条件变量。 在创建两个条件变量之间也能修改它。 当创建条件变量后,可以删除属性对象而不影响由它创建的条件变量。

"条件" 属性

支持下面的条件属性:

进程共享
指定条件变量的进程共享。 此属性取决于进程共享 POSIX 选项

创建和破坏条件变量

通过调用 pthread_cond_init 子例程来创建条件变量。 您可以指定条件属性对象。 如果您指定了 NULL 指针,条件变量将会有缺省属性。 因而,以下代码片段:
pthread_cond_t cond;
pthread_condattr_t attr;
...
pthread_condattr_init(&attr);
pthread_cond_init(&cond, &attr);
pthread_condattr_destroy(&attr);
等同于以下代码:
pthread_cond_t cond;
...
pthread_cond_init(&cond, NULL);

已创建的条件变量的标识通过 condition 参数被返回给调用线程。 条件标识是不透明对象;其类型为 pthread_cond_t 。 在 AIX中, pthread_cond_t 数据类型是一种结构; 在其他系统上,它可以是指针或其他数据类型。

条件变量必须被创建一次。 避免使用相同的 condition 参数多次调用 pthread_cond_init 子例程 (例如,在同时执行相同代码的两个线程中)。 可以通过下面的方法确保新创建的条件变量的唯一性:
  • 在创建将要使用此变量的其他线程之前调用 pthread_cond_init 子例程;例如,在初始线程中。
  • 在一次性初始化例程中调用 pthread_cond_init 子例程。 有关更多信息,请参阅 一次性初始化
  • 使用由 PTHREAD_COND_INITIALIZER 静态初始化宏初始化的静态条件变量; 该条件变量将具有缺省属性。

不再需要条件变量后,通过调用 pthread_cond_destroy 子例程将其销毁。 此子例程可能回收 pthread_cond_init 子例程分配的任何存储空间。 删除条件变量之后,可再次使用同一个 pthread_cond_t 变量创建另一个条件。 例如,下面的代码片段是有效的(尽管不是十分实用):

pthread_cond_t cond;
...
for (i = 0; i < 10; i++) {

        /* creates a condition variable */
        pthread_cond_init(&cond, NULL);

        /* uses the condition variable */

        /* destroys the condition */
        pthread_cond_destroy(&cond);
}

如同任何能够在线程间共享的系统资源,被分配到线程的堆栈上的条件变量必须在线程终止之前被删除。 线程库维护条件变量的已链接列表;因此,如果被分配互斥对象的堆栈被释放,列表将被毁坏。

使用条件变量

条件变量必须始终与互斥对象一起使用。 给定的条件变量只能与一个互斥对象关联,但是互斥对象可用于多个条件变量。 条件、互斥对象和条件变量可能捆绑为结构,如下面的代码片段所示:
struct condition_bundle_t {
        int              condition_predicate;
        pthread_mutex_t  condition_lock;
        pthread_cond_t   condition_variable;
};

等待条件

保护条件的互斥对象必须在等待条件前被锁定。 线程可以通过调用 pthread_cond_waitpthread_cond_timedwait 子例程来等待发出条件的信号。 子例程以不可分割的方式解锁互斥对象并阻塞调用线程,直到条件收到信号。 当调用返回时,互斥对象再次被锁定。

pthread_cond_wait 子例程无限期阻塞线程。 如果条件永不收到信号,那么线程将永不唤醒。 因为 pthread_cond_wait 子例程提供取消点,所以如果启用可取消,那么退出此死锁的唯一方法是取消被阻塞的线程。 有关更多信息,请参阅 取消线程

pthread_cond_timedwait 子例程仅在指定的时间段阻塞线程。 此子例程有额外的参数 timeout,指定休眠必须终止的绝对日期。 timeout 参数是指向 timespec 结构的指针。 此数据类型也称为 timestruc_t。 它包含以下字段:

tv_sec
无符号长整型整数(指定秒数)
tv_nsec
长整型整数(指定十亿分之一秒数)
通常,pthread_cond_timedwait 子例程以下列方式使用:
struct timespec timeout;
...
time(&timeout.tv_sec);
timeout.tv_sec += MAXIMUM_SLEEP_DURATION;
pthread_cond_timedwait(&cond, &mutex, &timeout);

timeout 参数指定绝对日期。 前面的代码片段说明如何指定一段时间而不是绝对日期。

要将 pthread_cond_timedwait 子例程与绝对日期配合使用,可以使用 mktime 子例程来计算tv_sectimespec 结构的字段。 在下面的示例中,线程会一直等待条件,直到本地时间 2001 年 1 月 1 日 08:00:
struct tm       date;
time_t          seconds;
struct timespec timeout;
...

date.tm_sec = 0;
date.tm_min = 0;
date.tm_hour = 8;
date.tm_mday = 1;
date.tm_mon = 0;         /* the range is 0-11 */
date.tm_year = 101;      /* 0 is 1900 */
date.tm_wday = 1;        /* this field can be omitted -
                            but it will really be a Monday! */
date.tm_yday = 0;        /* first day of the year */
date.tm_isdst = daylight;
        /* daylight is an external variable - we are assuming
           that Daylight Saving Time will still be used... */

seconds = mktime(&date);

timeout.tv_sec = (unsigned long)seconds;
timeout.tv_nsec = 0L;

pthread_cond_timedwait(&cond, &mutex, &timeout);

pthread_cond_timedwait 子例程也提供了一个取消点,尽管睡眠不是无限期的。 因而,不管休眠是否有超时,休眠的线程均可被取消。

发信号通知条件

可以通过调用 pthread_cond_signalpthread_cond_broadcast 子例程来指示条件。

pthread_cond_signal 子例程至少唤醒一个当前在指定条件上被阻塞的线程。 根据调度策略选择唤醒线程; 它是调度优先级最高的线程 (请参阅 调度策略和优先级)。 在多处理器系统或某些非AIX 系统上,可能会发生多个线程被唤醒的情况。 请不要假定此子例程只确切地唤醒一个线程。

pthread_cond_broadcast 子例程唤醒当前被阻塞在指定的条件下的每一个线程。 但是,线程能够在调用子例程返回后即开始等待相同的条件。

对这些例程的调用总是会成功的,除非指定了无效的 cond 参数。 这并不意味线程已经被唤醒。 而且,发信号通知条件不会被库记住。 例如,考虑条件 C。 没有线程在此条件下等待。 在时间 t ,线程 1 向条件 C 发出信号。 虽然未唤醒任何线程,但调用成功。 在时间 t+1,线程 2 使用 C 作为 cond 参数来调用 pthread_cond_wait 子例程。 线程 2 被阻塞。 如果没有其他线程发出信号 C,线程 2 可能一直等待,直到进程终止。

您能够通过在删除条件变量时检查 pthread_cond_destroy 子例程返回的 EBUSY 错误代码来避免这种死锁,如以下代码片段所示:

pthread_yield 子例程使另一个线程有机会被调度; 例如,其中一个已唤醒的线程。 获取有关 pthread_yield 子例程的更多信息。

pthread_cond_waitpthread_cond_broadcast 子例程不可用在一个信号处理程序中。 为方便线程等待信号,线程库提供了 sigwait 子例程。 获取有关 sigwait 子例程的更多信息。 有关 sigwait 子例程的更多信息,请参阅 信号管理

使用条件变量来同步线程

while (pthread_cond_destroy(&cond) == EBUSY) {
        pthread_cond_broadcast(&cond);
        pthread_yield();
}
条件变量被用于等待,直到特定的条件谓词变为真。 此条件谓词由另一个线程设置,通常是发信号通知条件的线程。

条件等待语义

条件谓词必须被互斥对象保护。 等待条件时,等待子例程( pthread_cond_wait 或者 pthread_cond_timedwait 子例程)以不可分割的方式解锁互斥对象并阻塞线程。 当发信号通知条件时,互斥被重新锁定并返回等待子例程。 请注意当子例程没有错误返回时,谓词可能仍然为假。

原因是可能不止一个线程被唤醒:称为 pthread_cond_broadcast 的子例程,或者是同时唤醒两个子例程的两个处理器间的不可避免的竞争。 锁定互斥对象的第一个线程将阻塞等待子例程中的所有其他被唤醒的线程,直到互斥对象被程序解锁。 因而,当第二个线程获取互斥对象并从等待子例程返回时,谓词可能已经更改。

总之,无论何时条件等待返回时,线程应该重新计算谓词以确定它是否能够安全地继续、应该再次等待还是应该声明超时。 从等待子例程返回并不意味着谓词为真或者假。

建议将条件等待包括在检查谓词的“while 循环”中。 下面的代码片段中说明了条件等待的基本实现:
pthread_mutex_lock(&condition_lock);
while (condition_predicate == 0)
        pthread_cond_wait(&condition_variable, &condition_lock);
...
pthread_mutex_unlock(&condition_lock);

计时等待语义

pthread_cond_timedwait 子例程返回超时错误时,因为超时过期和谓词状态更改之间另一次不可避免的竞争,谓词可能为真。

仅对于非计时等待,在出现超时时,线程应该重新计算谓词以决定它应该声明超时还是应该在任何情况下继续。 建议您在 pthread_cond_timedwait 子例程返回时仔细检查所有可能的情况。 下面的代码片段显示了如何在健壮的程序中实现这种检查:
int result = CONTINUE_LOOP;

pthread_mutex_lock(&condition_lock);
while (result == CONTINUE_LOOP) {
        switch (pthread_cond_timedwait(&condition_variable,
                &condition_lock, &timeout)) {

                case 0:
                if (condition_predicate)
                        result = PROCEED;
                break;

                case ETIMEDOUT:
                result = condition_predicate ? PROCEED : TIMEOUT;
                break;

                default:
                result = ERROR;
                break;
        }
}

...
pthread_mutex_unlock(&condition_lock);

result 变量能够用于选择操作。 解锁互斥锁之前的语句应尽快完成,因为不应长时间持有互斥锁。

timeout 参数中指定绝对日期将允许实时行为的简单实现。 如果在循环中使用多次,不需重新计算绝对超时,例如包括条件等待的绝对超时。 在系统时钟被操作员间断调快的情况下,使用绝对超时来确保在系统时间指定比 timeout 参数晚的日期时马上结束定时等待。

条件变量用法示例

下面的示例提供同步点例程的源代码。 同步点是程序中的指定点,在该处不同的线程必须等待,直到所有线程(或者至少一定数量的线程)已经到达该点。

同步点能够通过计数器(其受时钟保护)和条件变量简单实现。 如果计数器没有到达其最大值,那么每个线程均使用锁、增加计数器并等待发出信号通知条件。 否则,那么广播条件,那么所有的线程能够继续。 调用例程的最后一个线程广播条件。
#define SYNC_MAX_COUNT  10

void SynchronizationPoint()
{
        /* use static variables to ensure initialization */
        static mutex_t sync_lock = PTHREAD_MUTEX_INITIALIZER;
        static cond_t  sync_cond = PTHREAD_COND_INITIALIZER;
        static int sync_count = 0;

        /* lock the access to the count */
        pthread_mutex_lock(&sync_lock);

        /* increment the counter */
        sync_count++;

        /* check if we should wait or not */
        if (sync_count < SYNC_MAX_COUNT)

             /* wait for the others */
             pthread_cond_wait(&sync_cond, &sync_lock);

        else

            /* broadcast that everybody reached the point */
            pthread_cond_broadcast(&sync_cond);

        /* unlocks the mutex - otherwise only one thread
                will be able to return from the routine! */
        pthread_mutex_unlock(&sync_lock);
}

此例程有一些限制: 它只能使用一次,调用例程的线程数由符号常量编码。 但是,此示例显示了条件变量的基本用法。 获取更复杂的用法示例。 有关更复杂的用法示例,请参阅 创建复杂同步对象