条件变量属性:
使用条件变量可以以原子方式阻塞线程,知道某个特定条件为真为止。条件变量始终与互斥锁一起使用。
使用条件变量,线程可以以原子方式阻塞,知道满足某个条件为止。对掉件的测试时在互斥锁的保护下进行的。
如果条件为假,县城通常会给予条件变量则阻塞,并于原子方式释放等待条件变化的互斥锁。如果一个线程更改了条件,该线程可能会向相关的条件变量发出信号,从而是一个或多个等待的线程执行以下操作:
唤醒
再次获取互斥锁
重新评估条件
以下情况下,条件变量可用于在进程之间同步线程:
线程是在可以写入的内存中分配的
内存由协作进程共享
调度策略可确定唤醒阻塞线程的方式,对于缺省值,按照优先级顺序唤醒线程。
必须设置和初始化条件变量的属性,然后才能使用条件变量。
初始化条件变量属性:
使用pthread_condattr_init(3C) 可以将与该对象相关联的属性初始化为其缺省值。在执行过程中,线程系统会为每个属性对象分配存储空间。
pthread_condattr_init语法
int pthread_cond_init(pthread_cond_t *ptr , const othread_condattr_t *av);
cattr 设置为NULL。将cattr 设置为NULL 与传递缺省条件变量属性对象的地址等效,但是
没有内存开销。
使用PTHREAD_COND_INITIALIZER 宏可以将以静态方式定义的条件变量初始化为其缺省
属性。PTHREAD_COND_INITIALIZER宏与动态分配具有null 属性的pthread_cond_init()
等效,但是不进行错误检查。
多个线程决不能同时初始化或重新初始化同一个条件变量。如果要重新初始化或销毁
某个条件变量,则应用程序必须确保该条件变量未被使用。
pthread_cond_init返回值
pthread_cond_init()在成功完成之后会返回零。其他任何返回值都表示出现了错误。
如果出现以下任一情况,该函数将失败并返回对应的值。
EINVAL
描述: cattr 指定的值无效。
EBUSY
描述: 条件变量处于使用状态。
EAGAIN
描述: 必要的资源不可用。
ENOMEM
描述: 内存不足,无法初始化条件变量。
基于条件变量阻塞
使用pthread_cond_wait(3C)可以以原子方式释放mp 所指向的互斥锁,并导致调用线
程基于cv 所指向的条件变量阻塞。
pthread_cond_wait 语法
int pthread_cond_wait(pthread_cond_t *cv,pthread_mutex_t *mutex);
1
2
#include pthread_cond_t cv;
pthread_mutex_t mp;int ret;
/* wait on condition variable */ret = pthread_cond_wait(&cv, &mp);
阻塞的线程可以通过pthread_cond_signal() 或pthread_cond_broadcast()唤醒,也可
以在信号传送将其中断时唤醒。
不能通过pthread_cond_wait() 的返回值来推断与条件变量相关联的条件的值的任何变
化。必须重新评估此类条件。
pthread_cond_wait()例程每次返回结果时调用线程都会锁定并且拥有互斥锁,即使返
回错误时也是如此。
该条件获得信号之前,该函数一直被阻塞。该函数会在被阻塞之前以原子方式释放相
关的互斥锁,并在返回之前以原子方式再次获取该互斥锁。
通常,对条件表达式的评估是在互斥锁的保护下进行的。如果条件表达式为假,线程
会基于条件变量阻塞。然后,当该线程更改条件值时,另一个线程会针对条件变量发
出信号。这种变化会导致所有等待该条件的线程解除阻塞并尝试再次获取互斥锁。
必须重新测试导致等待的条件,然后才能从pthread_cond_wait()处继续执行。唤醒的
线程重新获取互斥锁并从pthread_cond_wait()返回之前,条件可能会发生变化。等待
线程可能并未真正唤醒。建议使用的测试方法是,将条件检查编写为调用
pthread_cond_wait() 的while()循环。
pthread_mutex_lock();
while(condition_is_false)
pthread_cond_wait();
pthread_mutex_unlock();
如果有多个线程基于该条件变量阻塞,则无法保证按特定的顺序获取互斥锁。
注– pthread_cond_wait() 是取消点。如果取消处于暂挂状态,并且调用线程启用了取
消功能,则该线程会终止,并在继续持有该锁的情况下开始执行清除处理程序。
pthread_cond_wait 返回值
pthread_cond_wait()在成功完成之后会返回零。其他任何返回值都表示出现了错误。
如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述: cv 或mp 指定的值无效。
解除阻塞一个线程
对于基于cv 所指向的条件变量阻塞的线程,使用pthread_cond_signal(3C)可以解除阻
塞该线程。
pthread_cond_signal语法
int pthread_cond_signal(pthread_cond_t *cv);
1
2
#include pthread_cond_t cv;int ret;/* one condition variable is signaled */ret = pthread_cond_signal(&cv);
应在互斥锁的保护下修改相关条件,该互斥锁用于获得信号的条件变量中。否则,可
能在条件变量的测试和pthread_cond_wait()阻塞之间修改该变量,这会导致无限期等
待。
调度策略可确定唤醒阻塞线程的顺序。对于SCHED_OTHER,将按优先级顺序唤醒线程。
如果没有任何线程基于条件变量阻塞,则调用pthread_cond_signal() 不起作用。
示例 使用pthread_cond_wait() 和pthread_cond_signal()
pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;
decrement_count()
{
pthread_mutex_lock(&count_lock);
while (count == 0)
pthread_cond_wait(&count_nonzero, &count_lock);
count = count - 1;
pthread_mutex_unlock(&count_lock);
}
increment_count()
{
pthread_mutex_lock(&count_lock);
if (count == 0)
pthread_cond_signal(&count_nonzero);
count = count + 1;
pthread_mutex_unlock(&count_lock);
}
pthread_cond_signal返回值
pthread_cond_signal()在成功完成之后会返回零。其他任何返回值都表示出现了错
误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述: cv 指向的地址非法。
示例 说明了如何使用pthread_cond_wait()和pthread_cond_signal()。
在指定的时间之前阻塞
pthread_cond_timedwait(3C) 的用法与pthread_cond_wait()的用法基本相同,区别在
于在由abstime 指定的时间之后pthread_cond_timedwait()不再被阻塞。
pthread_cond_timedwait语法
int pthread_cond_timedwait(pthread_cond_t *cv,
pthread_mutex_t *mp, const struct timespec *abstime);
1
2
3
#include #include pthread_cond_t cv;
pthread_mutex_t mp;
timestruct_t abstime;int ret;/* wait on condition variable */ret = pthread_cond_timedwait(&cv, &mp, &abstime);
1
2
3
4
5
6
7
8
9
pthread_cond_timewait()每次返回时调用线程都会锁定并且拥有互斥锁,即使
pthread_cond_timedwait()返回错误时也是如此。
pthread_cond_timedwait() 函数会一直阻塞,直到该条件获得信号,或者最后一个参数
所指定的时间已过为止。
注– pthread_cond_timedwait()也是取消点。
示例计时条件等待
pthread_timestruc_t to;
pthread_mutex_t m;
pthread_cond_t c;
...
pthread_mutex_lock(&m);
to.tv_sec = time(NULL) + TIMEOUT;
to.tv_nsec = 0;
while (cond == FALSE) {
err = pthread_cond_timedwait(&c, &m, &to);
if (err == ETIMEDOUT) {
/* timeout, do something */
break;
}
}
pthread_mutex_unlock(&m);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pthread_cond_timedwait 返回值
pthread_cond_timedwait() 在成功完成之后会返回零。其他任何返回值都表示出现了错
误。如果出现以下任一情况,该函数将失败并返回对应的值。
EINVAL
描述: cv 或abstime 指向的地址非法。
ETIMEDOUT
描述: abstime 指定的时间已过。
超时会指定为当天时间,以便在不重新计算值的情况下高效地重新测试条件,如示例
中所示。
在指定的时间间隔内阻塞
pthread_cond_reltimedwait_np(3C) 的用法与pthread_cond_timedwait()的用法基本相
同,唯一的区别在于pthread_cond_reltimedwait_np()会采用相对时间间隔而不是将来
的绝对时间作为其最后一个参数的值。
pthread_cond_reltimedwait_np语法
int pthread_cond_reltimedwait_np(pthread_cond_t *cv, pthread_mutex_t *mp,
const struct timespec *reltime);
1
2
3
#include #include pthread_cond_t cv;
pthread_mutex_t mp;
timestruct_t reltime;int ret;/* wait on condition variable */ret = pthread_cond_reltimedwait_np(&cv, &mp, &reltime);
1
2
3
4
5
6
7
8
pthread_cond_reltimedwait_np() 每次返回时调用线程都会锁定并且拥有互斥锁,即使
pthread_cond_reltimedwait_np()返回错误时也是如此。pthread_cond_reltimedwait_np()函数会一直阻塞,直到该条件获得信号,或者最后一个参数指定的时间间隔已过为止。
注– pthread_cond_reltimedwait_np()也是取消点。
pthread_cond_reltimedwait_np返回值
pthread_cond_reltimedwait_np()在成功完成之后会返回零。其他任何返回值都表示出
现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。
EINVAL
描述: cv 或reltime 指示的地址非法。
ETIMEDOUT
描述: reltime 指定的时间间隔已过。
解除阻塞所有线程
对于基于cv 所指向的条件变量阻塞的线程,使用pthread_cond_broadcast(3C)可以解
除阻塞所有这些线程,这由pthread_cond_wait()来指定。
pthread_cond_broadcast 语法
int pthread_cond_broadcast(pthread_cond_t *cv);
1
2
#include pthread_cond_t cv;int ret;
/* all condition variables are signaled */ret = pthread_cond_broadcast(&cv);
1
2
3
4
5
6
如果没有任何线程基于该条件变量阻塞,则调用pthread_cond_broadcast()不起作
用。
由于pthread_cond_broadcast() 会导致所有基于该条件阻塞的线程再次争用互斥锁,
因此请谨慎使用pthread_cond_broadcast()。例如,通过使用
pthread_cond_broadcast(),线程可在资源释放后争用不同的资源量,如示例 中
所示。
示例条件变量广播
pthread_mutex_t rsrc_lock;
pthread_cond_t rsrc_add;unsigned int resources;
get_resources(int amount)
{
pthread_mutex_lock(&rsrc_lock);
while (resources < amount) {
pthread_cond_wait(&rsrc_add, &rsrc_lock);
}
resources -= amount;
pthread_mutex_unlock(&rsrc_lock);
}
add_resources(int amount)
{
pthread_mutex_lock(&rsrc_lock);
resources += amount;
pthread_cond_broadcast(&rsrc_add);
pthread_mutex_unlock(&rsrc_lock);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
请注意,在add_resources()中,首先更新resources 还是首先在互斥锁中调用
pthread_cond_broadcast()无关紧要。
应在互斥锁的保护下修改相关条件,该互斥锁用于获得信号的条件变量中。否则,可
能在条件变量的测试和pthread_cond_wait()阻塞之间修改该变量,这会导致无限期等
待。
pthread_cond_broadcast 返回值
pthread_cond_broadcast()在成功完成之后会返回零。其他任何返回值都表示出现了错
误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述: cv 指示的地址非法。
销毁条件变量状态
使用pthread_cond_destroy(3C) 可以销毁与cv 所指向的条件变量相关联的任何状态。
pthread_cond_destroy 语法
int pthread_cond_destroy(pthread_cond_t *cv);
1
2
#include pthread_cond_t cv;int ret;
/* Condition variable is destroyed */ret = pthread_cond_destroy(&cv);
1
2
3
4
5
6
请注意,没有释放用来存储条件变量的空间。
pthread_cond_destroy返回值
pthread_cond_destroy()在成功完成之后会返回零。其他任何返回值都表示出现了错
误。如果出现以下情况,该函数将失败并返回对应的值。
EINVAL
描述: cv 指定的值无效。
唤醒丢失问题
如果线程未持有与条件相关联的互斥锁,则调用pthread_cond_signal() 或
pthread_cond_broadcast()会产生唤醒丢失错误。
满足以下所有条件时,即会出现唤醒丢失问题:
■ 一个线程调用pthread_cond_signal()或pthread_cond_broadcast()
■ 另一个线程已经测试了该条件,但是尚未调用pthread_cond_wait()
■ 没有正在等待的线程信号不起作用,因此将会丢失,仅当修改所测试的条件但未持有
与之相关联的互斥锁时,才会出现此问题。只要仅在持有关联的互斥锁同时修改所测试
的条件,即可调用pthread_cond_signal() 和pthread_cond_broadcast(),
而无论这些函数是否持有关联的互斥锁。
生成方和使用者问题
并发编程中收集了许多标准的众所周知的问题,生成方和使用者问题只是其中的一个
问题。此问题涉及到一个大小限定的缓冲区和两类线程(生成方和使用者),生成方
将项放入缓冲区中,然后使用者从缓冲区中取走项。
生成方必须在缓冲区中有可用空间之后才能向其中放置内容。使用者必须在生成方向
缓冲区中写入之后才能从中提取内容。
条件变量表示一个等待某个条件获得信号的线程队列。
示例中包含两个此类队列。一个队列(less) 针对生成方,用于等待缓冲区中出现空
位置。另一个队列(more) 针对使用者,用于等待从缓冲槽位的空位置中提取其中包含
的信息。该示例中还包含一个互斥锁,因为描述该缓冲区的数据结构一次只能由一个
线程访问。
示例生成方和使用者的条件变量问题
typedef struct {
char buf[BSIZE];
int occupied;
int nextin;
int nextout;
pthread_mutex_t mutex;
pthread_cond_t more;
pthread_cond_t less;
} buffer_t;
buffer_t buffer;
1
2
3
4
5
6
7
8
9
10
如示例 中所示,生成方线程获取该互斥锁以保护buffer 数据结构,然后,缓冲区
确定是否有空间可用于存放所生成的项。如果没有可用空间,生成方线程会调用
pthread_cond_wait()。pthread_cond_wait()会导致生成方线程连接正在等待less 条件
获得信号的线程队列。less 表示缓冲区中的可用空间。
与此同时,在调用pthread_cond_wait()的过程中,该线程会释放互斥锁的锁定。正在
等待的生成方线程依赖于使用者线程在条件为真时发出信号,如示例 中所示。该
条件获得信号时,将会唤醒等待less 的第一个线程。但是,该线程必须再次锁定互斥
锁,然后才能从pthread_cond_wait()返回。
获取互斥锁可确保该线程再次以独占方式访问缓冲区的数据结构。该线程随后必须检
查缓冲区中是否确实存在可用空间。如果空间可用,该线程会向下一个可用的空位置
中进行写入。
与此同时,使用者线程可能正在等待项出现在缓冲区中。这些线程正在等待条件变量
more。刚在缓冲区中存储内容的生成方线程会调用pthread_cond_signal() 以唤醒下一
个正在等待的使用者。如果没有正在等待的使用者,此调用将不起作用。
最后,生成方线程会解除锁定互斥锁,从而允许其他线程处理缓冲区的数据结构。
示例生成方和使用者问题:生成方
void producer(buffer_t *b, char item)
{
pthread_mutex_lock(&b->mutex);
while (b->occupied >= BSIZE)
pthread_cond_wait(&b->less, &b->mutex);
assert(b->occupied < BSIZE);
b->buf[b->nextin++] = item;
b->nextin %= BSIZE;
b->occupied++;
/* now: either b->occupied < BSIZE and b->nextin is the index
of the next empty slot in the buffer, or
b->occupied == BSIZE and b->nextin is the index of the
next (occupied) slot that will be emptied by a consumer
(such as b->nextin == b->nextout) */
pthread_cond_signal(&b->more);
pthread_mutex_unlock(&b->mutex);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
请注意assert()语句的用法。除非在编译代码时定义了NDEBUG,否则assert() 在其参
数的计算结果为真(非零)时将不执行任何操作。如果参数的计算结果为假(零),
则该程序会中止。在多线程程序中,此类断言特别有用。如果断言失败,assert()会
立即指出运行时问题。assert() 还有另一个作用,即提供有用的注释。
以/* now: either b->occupied ...开头的注释最好以断言形式表示,但是由于语句过
于复杂,无法用布尔值表达式来表示,因此将用英语表示。
断言和注释都是不变量的示例。这些不变量是逻辑语句,在程序正常执行时不应将其
声明为假,除非是线程正在修改不变量中提到的一些程序变量时的短暂修改过程中。
当然,只要有线程执行语句,断言就应当为真。
使用不变量是一种极为有用的方法。即使没有在程序文本中声明不变量,在分析程序
时也应将其视为不变量。
每次线程执行包含注释的代码时,生成方代码中表示为注释的不变量始终为真。如果
将此注释移到紧挨mutex_unlock()的后面,则注释不一定仍然为真。如果将此注释移
到紧跟assert()之后的位置,则注释仍然为真。
因此,不变量可用于表示一个始终为真的属性,除非一个生成方或一个使用者正在更
改缓冲区的状态。线程在互斥锁的保护下处理缓冲区时,该线程可能会暂时声明不变
量为假。但是,一旦线程结束对缓冲区的操作,不变量即会恢复为真。
示例给出了使用者的代码。该逻辑流程与生成方的逻辑流程相对称。
示例生成方和使用者问题:使用者
char consumer(buffer_t *b)
{
char item;
pthread_mutex_lock(&b->mutex);
while(b->occupied <= 0)
pthread_cond_wait(&b->more, &b->mutex);
assert(b->occupied > 0);
item = b->buf[b->nextout++];
b->nextout %= BSIZE;
b->occupied--;
/* now: either b->occupied > 0 and b->nextout is the index
of the next occupied slot in the buffer, or
b->occupied == 0 and b->nextout is the index of the next
(empty) slot that will be filled by a producer (such as
b->nextout == b->nextin) */
pthread_cond_signal(&b->less);
pthread_mutex_unlock(&b->mutex);
return(item);
}