POSIX 线程同步——条件变量

POSIX多线程程序设计. 作者 美 David R.Buten

条件变量是用来通知共享数据状态信息的。可以使用条件变量来通知队列已空、或队列非空、或任何其他需要由线程处理的共享数据状态。

当一个线程互斥地访问共享状态时,它可能发现在其他线程改变状态之前它什么也做不了。状态可能是对的和一致的,即没有破坏不变量,但但是线程就是对当前状态不感兴趣。例如,一个处理队列的线程发现队列为空时,它只能等待,直到有一个节点被填加进队列中。

例如,共享数据由一个互斥量保护。线程必须锁住互斥量来判定队列的当前状态,如判断队列是否为空。线程在等待之前必须释放锁(否则)其他线程就不可能插入数据),然后等待队列状态的变化。例如,线程可以通过某种方式阻塞自己,以便插入线程能够找到它的ID并唤醒它。但是这里有一个问题,即线程是运行于解锁和阻塞之间。

如果线程仍在运行,而其他线程此时向队列中插入了一个新的元素,则其他线程无法确定是否有线程在等待新元素。等待线程已经查找了队列并发现队列为空,解锁互斥量,然后阻塞自己,所以无法知道队列不再为空。更糟糕的是,它可能没有说明它在等待队列非空,所以其他线程无法找到它的线程ID,它只有永远等下去了。解锁和等待操作必须是原子性的,以防止其他线程在该线程解锁之后、阻塞之前锁住互斥量,这样其他线程才能够唤醒它。

等待条件变量总是返回锁住的互斥量。

这就是为什么要使用条件变量的原因。条件变量是与互斥量相关、也与互斥量保护的共享数据相关的信号机制。在一个条件变量上等待会导致以下原子操作:释放相关互斥量,等待其他线程发给该条件变量的信号(唤醒一个等待者)或广播该条件变量(唤醒所有等待者)。当等待条件变量时,互斥量必须治终锁住:当线程从条件变量等待中醒来时,它重新继续锁住互斥量。

条件变量不提供互斥。需要一个互斥量来同步对共享数据(包括活等待的谓词)的访问,这就是为什么在等待条件变量时必须指定一个互斥量。通过将解锁操作与等待条件变量原子化,Pthreads系统确保了在释放互斥量和等待条件变量之间没有线程可以改变与条件变量相关的“谓词”(如队列满或者队列空)。

为什么不将互斥量作为条件变量的一部分来创建呢?首先,互斥量不仅与条件变量一起使用,而且还要单独使用;其次,通常一个互斥量可以与多个条件变量相关。例如,队列可以为空,也可以为满。虽然可以设置两个条件变量让线程等待不同的条件,但只能有一个互斥量来协调对队列头的访问。

一个条件变量应该与一个谓词相关。如果试图将一个条件变量与多个谓词相关,或者将多个条件变量与一个谓词相关,就有陷入死锁或者竞争问题的危险。只要小心使用,可能不会有什么问题,但是很容易搞混你的程序,并且通常也不值得冒险。原则是:第一,当你在多个谓词之间共享一个条件变量时,必须总是使用广播,而不是发信号;第二,信号要比广播有效。

条件变量和谓词都是程序中的共享数据。它们被多个线程使用,可能是同时使用。由于你认为条件变量和谓词总是一起被锁定的,所以容易让人记住它们总是被相同的互斥量控制。在没有锁住互斥量前就发信号或广播条件变量是可能的(合法的,通常也是合理的),但是更安全的方式是先锁住互斥量。

下图显示了三个线程与一个条件变量交互的时间图。圆形框代表条件变量,三条线段代表三个线程的活动。

当线段进入圆形框时,既表明线程使用条件变量做了一些事。当线程对应的线段在到达圆形框中线之前停止,表明线程在等待条件变量;当线程线段到达中线之下时,表明它在发信号或广播来唤醒等待线程。

线程1等条件变量发信号,由于此时没有等待线程,所以没有任何效果。线程1然后在条件变量上等待。线程2同样在条件变量上阻塞,随后线程3发信号唤醒在条件变量上等待的线程1。线程3然后在条件变量上等待。线程1广播条件变量,唤醒线程2和线程3。随后,线程3在条件变量上等待。一段时间后,线程3的等待时间超时,线程3被唤醒。

1 创建和释放条件变量

pthread_cond_t cod = PTHREAD_COND_INITIALIZER
int pthread_cond_init (pthread_cond_t *cond,
    pthread_condattr_t *condattr);
int Pthread_cond_destroy (pthread_cond_t *ccond)

程序中由pthread_cond_t类型的变量来表示条件变量。永远不要拷贝条件变量,因为使用条件变量的备份是不可知的,这就像是打一个断线的电话号码并等待回答一样。例如,一个线程可能在等待条件变量的一个拷贝,同时其他线程可能广播或发信号给该条件变量的其他拷贝,则该等待线程就不能被唤醒。不过,可以传递条件变量的指针以使不同函数和线程可以使用它来同步。

大部分时间你可能在整个文件范围内(即不在任何函数内部)声明全局或静态类型条件变量。如果有其他文件需要使用,则使用全局(extern)类型;否则,使用静态(static)类型。如下面实例cond_static.c所示,如果声明了一个使用默认属性值的静态条件变量,则需要使用PTHREAD_COND_INITIALIZER宏初始化。

#include <pthread.h>
#include "errors.h"
/*
 * Declare a structure, with a mutex and conddition variable
 * statically initialized. This is the same asusing
 * pthread_mutex_init and pthread_cond_init, with the ddefault
 * attributes.
 */
typedef struct_my_struct_tag {
    pthread_mutex_t mutex; /* Protects access to value */
    pthread_cond_t cond; /* Signals change to value */
    int value; /* Access protected by mutex */
} my_struct_t;
my_struct_t data = {
    PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER0};
int main (int argc, char *argv[]){
    return 0;
}

当声明条件变量时,要记住条件变量与相关的谓词是”链接”在一起的。建议你将一组不变量、谓词和它们的互斥量,以及一个或多个条件变量封装为一个数据结构的元素,并仔细地记录下它们之间的关系。

有时无法静态地初始化一个条件变量,例如,当使用malloc分配一个包含条件变量的结构时。这时,你需要调用pthread_cond_init来动态他初始化条件变量,如以下实例cond_dynamic.c所示。还可以动态初始化静态声明的条件变量,但是必须确保每个条件变量在使用之前初始化且仅初始化次。你可以在建立任何线程前初始化它,或者使用pthread_once。如果需要使用非默认属性初始化条件变量,必须使用动态初始化。

#include <pthread.h>
#include "errors.h"
/*
 * Define a structure, with a mutex and condittion variable.
 */
typedef struct_my_struct_tag {
    pthread_mutex_t mutex; /* Protects access to value */
    pthread_cond_t cond; /* Signals change to value */
    int value; /* Access protected by mutex */
} my_struct_t;
int main (int argc, char *argv[]) {
    my_struct_t *data;
    int status;
    data = malloc (sizeof (my_struct_t));
    if (data == NULL)
        errno_abort ("Allocate structure");
    status = pthread_mutex_init (&data->mutex, NULL);
    if (status != 0)
        err_abort (status, "Init mutex");
    status = pthread_cond_init (&data->cond, NULL);
    if (status != 0)
        err_abort (status, "Init condition");
    status = pthread_cond_destroy (&data->cond);
    if (status != 0)
        err_abort (status, "Destroy condition");
    status = pthread_mutex_destroy (&data->mutex);
    if (status != 0)
        err_abort (status, "Destroy mutex");
    (void)free (data);
    return status;
}

当动态初始化条件变量时,应该在不需要它时调用pthread_coind_destroy来释放它。不必释放一个通过PTHREAD_COND_INITIALIZER宏静不态初始化的条件变量。

当你确信没有其他线程在某条件变量上等待,或者将要等待、发信号或广播时,可以安全地释放该条件变量。判定上述情况的最好方式是在刚刚成功地广播了该条件变量、唤醒了所有等待线程的线程内,且确信不再有线程随后后使用它时安全释放。

2 等待条件变量

int pthread_cond_wait (pthread_cond_t *coned,
    pthread_mutex_t *mutex);
int pthread_cond_timedwait (pthread_cond t *cond, 
    pthread_mutex_t *mutex, struct timespec *expiration);

每个条件变量必须与一个特定的互斥量、一个谓词条件相关联。当线程等待条件变量时,它必须将相关互斥量锁住。记住,在阻塞线程之前,条件变量等待操作将解锁互斥量;而在重新返回线程之前,会再次锁住互斥量。

所有并发地(同时)等待同一个条件变量的线程必须指定同一个相关互斥量。例如,Pthreads不允许线程1使用互斥量A等待条件变量A,而线程2使用互斥量B等待条件变量A。不过,以下情况是十分合理的:线程1使用互斥量A等待条件变量A,而线程2使用互斥量A等待条件变量B。即有任何条件变量在特定时刻只能与一个互斥量相关联,而互斥量则可以同时与多个条件变量关联。

在锁住相关的互斥量之后和在等待条件变量之前,测试谓词是很重要的。如果线程发信号或广播一个条件变量,而没有线程在等待该条件变量时,则什么也没发生。如果在这之后,有线程调用pthread_cond_wait,则它将一直等待下去而无视该条件变量刚刚被广播的事实,这将意味着该线程可能永远不被唤醒。因为在线程等待条件变量之前,互斥量一直被锁住,所以,在测试谓词和等待条件变量之间无法设置谓词——互斥量被锁住,没有其他线程可以修改共享数据,包括谓词。

当线程醒来时,再次测试谓词同样重要。应该总是在循环中等待条件变量,来避免程序错误、多处理器竞争和假唤醒。以下实例cond.c,显示了如何等待条件变量。

wait_thread 线程睡眠一段时间以允许主线程在被唤醒之前条件变量等待操作,设置共享的谓词(data.value),然后发信号给条件变量。 wait_thread线程等待的时间由hibernation变量控制,默认是1秒。

如果程序带参数运行,则将该参数解析为整数值,保存在hibernation变量中。这将控制wait_thread线程在发送条件变量的信号前等待的时间。

主线程调用pthread_cond_timedwait函数等待至多2秒(从当前时间开始)。如果hibernation变量设置为大于两秒的值,则条件变量等待操作将超时,返回ETIMEDOUT。如果hibernation变量设为2秒,则主线程和wait_thread线程发生竞争,并且每次运行的结果可能不同。如果hibernation变量设置为小于2秒,则条件变量等待操作不会超时。

#include <pthread.h>
#include "errors.h"

typedef struct_my_struct_tag {
    pthread_mutex_t mutex; /* Protects access to value */
    pthread_cond_t cond; /* Signals change to value */
    int value; /* Access protected by mutex */
} my_struct_t;
my_struct_t data = {
    PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER0};

int hibernation = 1; /* Default to 1 second */
/*
 * Thread start routine. It will set the mainthread's predicate
 * and signal the condition variable.
 */
void * wait_thread (void *arg) {
    int status;
    sleep (hibernation);
    status = pthread_mutex_lock (&data.mutex);
    if (status != 0)
        err_abort (status, "Lock mutex");
    data.value = 1; /* Set predicate */
    status = pthread_cond_signal (&data.cond);
    if (status != 0)
        err_abort (status, "Signal condition");
    status = pthread_mutex_unlock (&data.mutex);
    if (status != 0)
        err_abort (status, "Unlock mutex");
    return NULL;
}

int main (int argc, char *argv[]) {
    int status;
    pthread_t wait_thread_id;
    struct timespec timeout;
    /* 
     * If an argument is specified, interpret it asthe number
     * of seconds for wait thread to sleep before signaling the
     * condition variable. You can play with this to see the
     * condition wait below time out or wake normally.
     */
    if (argc > 1)
        hibernation = atoi (argv[1]);
    /*
     * Create wait_thread.
     */
    status = pthread_create (
        &wait_thread_id, NULL, wait_thread, NULL);
    if (status != 0)
        err_abort (status, "Create wait thread");

    /*
     * Wait on the condition variable for 2 seconnds, or until
     * signaled by the wait_thread. Normally, wait_tthread
     * should signal. If you raise "hibernation" above 2
     * seconds, it will time out.
     */
    timeout.tv sec = time (NULL) + 2;
    timeout.tv nsec = 0;
    status = pthread_mutex_lock (&data.mutex);
    if (status != 0)
        err_abort (status, "Lock mutex");
    while (data.value == 0) {
        status = pthread_cond_timedwait (
            &data.cond, &data.mutex, &timeout);
        if (status == ETIMEDOUT) {
            printf ("Condition wait timed out.\n");
            break;
        }
        else if (status != 0)
            err_abort (status, "Wait on condition");
    }
    if (data.value 1= 0)
        printf ("Condition was signaled.\n");
    status = pthread_mutex_unlock (&data.mutex)1
    if (status != 0)
        err_abort (status, "Unlock mutex");
    return 0;
}

pthread_cond_wait函数是POSIX线程库中用于等待条件变量的函数之一。它的原理涉及到线程同步和互斥锁的概念。

在调用pthread_cond_wait之前,通常需要先获取一个互斥锁(pthread_mutex_lock),以确保在等待条件变量期间的线程安全性。然后,线程会检查一个条件,如果条件不满足,线程就会阻塞在pthread_cond_wait调用处,等待其他线程发出条件变量的信号。

当其他线程调用pthread_cond_signal或pthread_cond_broadcast函数时,条件变量会被发出信号。这些函数用于通知等待在条件变量上的线程,条件已经满足,或者在广播情况下,通知所有等待线程。此时,被阻塞的线程会被唤醒,并开始重新尝试获取互斥锁。

pthread_cond_wait的原理可以简述如下:

  1. 线程调用pthread_mutex_lock获取互斥锁,确保线程安全。
  2. 线程检查条件是否满足。如果条件满足,线程不会调用pthread_cond_wait,而是继续执行后续操作。
  3. 如果条件不满足,线程调用pthread_cond_wait,释放互斥锁并进入阻塞状态,等待条件变量的信号。
  4. 其他线程调用pthread_cond_signal或pthread_cond_broadcast发出条件变量的信号。
  5. 被阻塞的线程被唤醒,重新尝试获取互斥锁。
  6. 线程成功获取互斥锁后,继续执行后续操作。

需要注意的是,pthread_cond_wait函数的阻塞和唤醒是由操作系统内核实现的,因此具体的实现细节可能因操作系统而异。但是,POSIX线程库提供了一种标准接口,确保了跨平台的可移植性。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注