互斥量防止多个线程同时访问同一共享变量。

条件变量允许一个线程就某个条件(共享变量)的变化状态通知其他线程,并让其他线程等待(阻塞于)该通知。

条件变量与互斥量一起使用,允许线程以无竞争的方式等待特定的条件发生。

条件变量

条件一般是多线程共享的全局变量,由互斥量保护用于多线程同步,其中一个线程检测条件发生变化然后通知其他线程。

条件变量的主要操作是发送信号等待

发送信号操作即通知一个或多个处于等待状态的线程,条件的状态已经发生改变。

等待操作是指受到一个通知前一直处于阻塞状态。

初始化条件变量

条件变量描述
数据类型pthread_cond_t
静态分配pthread_cond_t cond=PTHREAD_COND_INITIALIZER
动态分配pthread_cond_init()pthread_cond_destroy()
#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
int pthread_cond_destroy(pthread_cond_t *cond);
// RETURN VALUE
// All condition variable functions return 0 on success and a non-zero error code on error

pthread_cond_init()接口参数cond_attrNULL时,使用默认参数。

通知条件变量

函数pthread_cond_signal()pthread_cond_broadcast可对由参数cond指定的条件变量而发送信号。

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
// RETURN VALUE
// All condition variable functions return 0 on success 
// and a non-zero error code on error

pthread_cond_signal()函数唤醒至少一条由于等待cond指定的信号而处于阻塞的线程,pthread_cond_broadcast()函数会唤醒所有阻塞的线程。

只有当仅需唤醒一条等待线程来处理条件(共享变量)的状态变化时,才应使用pthread_cond_signal(),因为这规避了pthread_cond_broadcast()同时唤醒多条等待线程进行竞争的时间消耗。

常见使用pthread_cond_signal()函数的情况是,所有等待线程都在执行相同的任务。

pthread_cond_signal()函数既可以位于pthread_mutex_lock()pthread_mutex_unlock()之间,也可以在其后,对于线程同步没有影响,但是效率上并不一致。

之间

pthread_mutex_lock(&mutex);
pthread_cond_signal(&cond, &mutex);
pthread_mutex_unlock(&mutex);
//...

调用pthread_cond_wait()的线程可能会在互斥量仍处于锁住状态醒来(内核空间),然后没有获得锁(用户空间),再次进入休眠状态(重新返回内核空间)。这可能会多出两个上下文切换的性能消耗。

但是在Linux Threads或NPTL的线程实现里面,只会出现等待队列转移

因为在Linux线程中,有两个队列,分别是cond_wait队列和mutex_lock队列,cond_signal只是让线程从cond_wait队列移到mutex_lock队列,而不用返回到用户空间,不会有性能的损耗。

之后

pthread_mutex_lock(&mutex);
// ...
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond, &mutex);

如果pthread_mutex_unlock()pthread_cond_signal()之间,有低优先级的线程正阻塞在mutex互斥量上,那么该低优先级的线程就会抢占高优先级的线程(阻塞在cond上的线程),而这在上面的放中间的模式下是不会出现的。

等待条件变量

函数pthread_cond_wait()pthread_cond_timedwait()用于阻塞线程,直到收到条件变量cond的通知或者到达指定的等待时间。

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, 
	const struct timespec *abstime);
// RETURN VALUE
// All condition variable functions return 0 on success 
// and a non-zero error code on error

abstime用于指定等待的绝对时间,即当前时间+等待时间。mutex参数为保护条件并且是锁住的互斥量

这两个函数具体要执行以下步骤:

  1. 解锁互斥量mutex
  2. 阻塞调用线程,直到其他线程就该条件变量cond发出信号。
  3. 重新锁定mutex

条件变量的使用方法一般为:

  1. 锁住互斥量mutex
  2. 检查条件。
  3. 条件不满足,则调用pthread_cond_wait()pthread_cond_timedwait()阻塞线程,等待条件满足。
  4. 收到信号,再次检查条件。条件满足则执行下一步任务,条件不满足返回步骤3。

条件变量使用范例:

if(pthread_mutex_lock(&mutex)!=0)
    exit(-1);
while(/*条件不满足*/)
    pthread_cond_wait(&cond, &mutex);

// other task

if(pthread_mutex_unlock(&mutex)!=0)
    exit(-1);

检查条件时锁住互斥量的原因在于条件属于共享变量,而检查条件和线程等待条件不属于原子操作

必须在while循环中检查条件的原因在于pthread_cond_wait()函数返回并不能代表条件满足,所以应该立即重新检查条件,不满足重新休眠等待

pthread_cond_wait()返回时不能对条件做任何假设的原因有:

  1. 其他线程可能会率先醒来改变条件。即使就条件变量发出通知的线程将条件设置为预期状态,也不能阻止这种情况的发生。
  2. 设计时应该用条件变量表征可能性而非确定性
  3. 可能会出现虚假唤醒的情况。没有其他线程就该条件变量发出信号,但是等待线程依然有可能醒来。

实例

在下面的示例中,条件是队列的状态,用互斥量保护条件,在while循环中判断体条件。

将消息放到工作队列时,需要占用互斥量,给线程发送信号时无需占用互斥量。

#include <pthread.h>
struct msg {
struct msg *m_next;
/* ... more stuff here ... */
};
struct msg *workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
void
process_msg(void)
{
	struct msg *mp;
	for (;;) {
	pthread_mutex_lock(&qlock);
	while (workq == NULL)
    	// 这儿加锁的原因在于保护条件,并保证条件检查和将进程放到等待条件队列属于原子操作。
    	// 否则条件检查完有可能出现其他线程改变条件,但当前线程还是会阻塞。
		pthread_cond_wait(&qready, &qlock);
	mp = workq;
	workq = mp->m_next;
	pthread_mutex_unlock(&qlock);
	/* now process the message mp */
	}
}
void
enqueue_msg(struct msg *mp)
{
	pthread_mutex_lock(&qlock);
	mp->m_next = workq;
	workq = mp;
	pthread_mutex_unlock(&qlock);
	// 这儿发信号放在解锁之后,可能会出现阻塞在qlock的其他线程先占有锁,并改变条件。
	// 但是由于是在while中检查条件,因此并不影响功能。
	// Linux中采用的是NPTL线程模型,因此推荐pthread_cond_signal在互斥量里面调用,
	// 不会产生解锁再阻塞的性能问题。
	pthread_cond_signal(&qready);
}