在上一篇文章中,主要描述了互斥锁的含义、属性、使用原则以及使用方法。互斥锁是有个不足就是只能用于线程间的互斥,不能用于“同步”,因此互斥锁往往不是独立使用的。本文描述条件变量的相关内容,因为条件变量往往是和互斥锁一起结合使用的,为什么会这样使用,详见下面章节。
条件变量(Condition variable)是线程间同步的一种机制。条件变量本质是一个信号量,一个线程阻塞等待某一条件,如果未获取的条件变量信号一直阻塞,直至获取到;另一个线程在达到预期条件时,产生一个通知信号给等待线程,等待线程被唤醒继续执行,达到同步的目的。
【1】 唤醒可能被拦截,多个线程等待条件变量时,由于系统的并发性,在线程A获取到条件变量前,线程B已获取到并重置了共享数据的状态;由于谓语已经不成立,线程A如果不检查谓语可能会导致不可预期的后果(访问空指针等)。
【2】增加程序健壮性
条件变量谓语:
谓语表示具有唯一真假值的语句,程序中可以用谓语来描述当前线程需要的条件状态。条件变量的关注对象是共享数据状态的变化,这一变化可以使用谓语来描述。 如果谓语为假,则等待条件变量信号;获取到条件变量后,再一次判断谓语真假,来决定是否执行任务。
条件变量使用的基本步骤为:
【1】创建条件变量实例
【2】初始化条件变量
【3】等待条件变量
【4】发送条件变量信号
【5】销毁条件变量
posix线程互斥锁以pthread_cond_t
数据结构表示。条件变量实例可以用静态和动态创建。
pthread_cond_t cond;
条件变量初始化可以使用pthread_cond_init
动态初始化,也可以使用宏 PTHREAD_COND_INITIALIZER
实现静态初始化, PTHREAD_COND_INITIALIZER
是POSIX定义的一个结构体常量 。
动态初始化
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
cond
,条件变量实例地址,不能为NULL
attr
,条件变量属性地址,传入NULL表示使用默认属性;大部分场合使用默认属性即可,关于属性详见第四节。
返回,成功返回0,参数无效返回 EINVAL
静态初始化
使用宏PTHREAD_COND_INITIALIZER
的静态初始化方式等价于使用pthread_cond_init
采用默认属性(attr
传入NULL)的动态初始化,不同之处在于PTHREAD_COND_INITIALIZER
宏没有相关错误参数的检查。
使用例子:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
条件变量获取分为无限阻塞方式和指定超时时间阻塞方式。
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
cond
,条件变量实例地址,不能为NULLmutex
,互斥锁实例地址,不能为NULLint pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,
const struct timespec *abstime);
cond
,条件变量实例地址,不能为NULLmutex
,互斥锁实例地址,不能为NULLabstime
,超时时间,单位为时钟节拍返回值 | 描述 |
---|---|
0 | 成功 |
EINVAL | 参数无效 |
EDEADLK | 互斥锁为非嵌套锁重复调用该函数 |
EBUSY | 锁被其他线程持有 |
ETIMEDOUT | 超时 |
该函数不会无限等待,超出指定时间节拍后,仍未等待到条件变量信号函数会返回,返回ETIMEDOUT
。
条件变量等待必须与互斥锁一起使用,因为系统的并发性,多个线程在等待获取条件变量时发生竞态。也可以这样理解,条件变量信号也是共享资源,需要互斥访问。互斥量等待参考伪代码:
pthread_mutex_lock(&mutex)
while(/* 谓语 */)
{
pthread_cond_wait(&cond, &mutex);
}
if (/* 谓语成立 */)
{
/* todo */
}
pthread_mutex_unlock(&mutex);
条件信号产生分为点对点方式和广播方式,点对点方式只能唤醒一个等待条件变量的线程,广播方式可以唤醒所有等待条件变量的线程。
int pthread_cond_signal(pthread_cond_t *cond);
cond
,条件变量实例地址,不能为NULLint pthread_cond_broadcast(pthread_cond_t *cond);
cond
,条件变量实例地址,不能为NULL条件变量信号产生前,对共享资源的访问,依然需要加锁处理,保护共享资源。典型使用参考伪代码:
pthread_mutex_lock(&mutex);
/* todo 访问共享资源 */
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond);
此外,等待线程和条件信号产生线程都加锁还有一个重要作用就是防止信号丢失。如果加锁信号产生线程先执行,并产生一个条件信号,此时等待线程还未执行,就“错过”了条件信号,条件信号丢失。加锁的情况下,条件信号产生线程必须申请到锁之后才会产生信号。
信号发送函数位置问题
pthread_mutex_lock(&mutex);
/* todo 访问共享资源 */
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond);
优点:无潜在的性能损耗问题
缺点:存在线程优先级翻转问题,低优先级抢占高优先级线程。如果unlock和signal之前,有个低优先级的线程(优先级比等待信号量线程低)申请持有互斥锁,此时该线程会先被唤醒调度,高优先级的等待信号量线程被抢占。
pthread_mutex_lock(&mutex);
/* todo 访问共享资源 */
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
优点:无线程优先级翻转问题
缺点: 潜在的性能损耗问题,降低效率
一般情况下,推荐使用第二种方式。
int pthread_cond_destroy(pthread_cond_t *cond);
cond
,条件变量实例地址,不能为NULL返回值 | 描述 |
---|---|
0 | 成功 |
EINVAL | cond已被销毁过,或者cond为空 |
EBUSY | 条件变量被其他线程使用 |
pthread_cond_destroy
用于销毁一个已经使用动态初始化的条件变量。销毁后的条件变量处于未初始化状态,条件变量的属性和控制块参数处于不可用状态。使用销毁函数需要注意几点:
pthread_cond_init
重新初始化使用PTHREAD_COND_INITIALIZER
静态初始化的互斥锁不能销毁代码实现功能:
#include
#include
#include
#include
#include "pthread.h"
struct _buff_node
{
uint8_t buf[64];
uint32_t occupy_size;
};
/* 静态方式创建初始化互斥锁和条件变量 */
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
/* 共享缓存 */
static struct _buff_node s_buf;
void *thread0_entry(void *data)
{/* 消费者线程 */
uint8_t i =0;
for (;;)
{
pthread_mutex_lock(&mutex);
while (s_buf.occupy_size == 0)
{
pthread_cond_wait(&cond, &mutex);
}
if (s_buf.occupy_size != 0)
{
printf("consume data:");
}
for (i=0; i<s_buf.occupy_size; i++)
{
printf("0x%02x ", s_buf.buf[i]);
}
s_buf.occupy_size = 0;
pthread_mutex_unlock(&mutex);
printf("\r\n");
}
}
void *thread1_entry(void *data)
{/* 生产者线程 */
uint8_t i =0;
for (;;)
{
pthread_mutex_lock(&mutex);
for (i = 0;i<8; i++)
{
s_buf.buf[i] = rand(); /* 随机生成数据 */
s_buf.occupy_size++;
}
pthread_mutex_unlock(&mutex);
printf("produce %d data\n", s_buf.occupy_size);
pthread_cond_signal(&cond);
sleep(1); /* 1秒生成一组数 */
}
}
int main(int argc, char **argv)
{
pthread_t thread0,thread1;
void *retval;
pthread_create(&thread0, NULL, thread0_entry, NULL);
pthread_create(&thread1, NULL, thread1_entry, NULL);
pthread_join(thread0, &retval);
pthread_join(thread1, &retval);
return 0;
}
输出结果
acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/cond$ gcc cond.c -o cond -lpthread
acuity@ubuntu:/mnt/hgfs/LSW/STHB/pthreads/cond$ ./cond
produce 8 data
consume data:0x67 0xc6 0x69 0x73 0x51 0xff 0x4a 0xec
produce 8 data
consume data:0x29 0xcd 0xba 0xab 0xf2 0xfb 0xe3 0x46
代码中,对printf
函数加锁,实际使用是不允许的,违反了加锁的原则,这里只是模拟场景测试。
使用默认的条件变量属性可以满足绝大部分的应用场景,特殊场景也可以调整条件变量属性。下面描述主要的条件变量属性及API。条件变量属性设置,基本步骤为:
【1】创建条件变量属性实例
【2】初始化属性实例
【3】设置属性
【4】销毁属性实例
posix线程条件变量属性以pthread_condattr_t
数据结构表示。条件变量属性实例可以用静态和动态创建。
pthread_condattr_t attr;
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
attr
,条件变量属性实例地址,不能为NULL 设置条件变量属性时,首先创建一个属性pthread_condattr_t
实例,然后调用pthread_condattr_init
函数初始实例,接下来就是属性设置。初始化后的属性值就是默认条件变量属性,等价于使用pthread_cond_init
采用默认属性(attr
传入NULL)的初始化。
条件变量作用域表示条件变量的作用范围,分为进程内(创建者)作用域PTHREAD_PROCESS_PRIVATE
和跨进程作用域PTHREAD_PROCESS_SHARED
。进程内作用域只能用于进程内线程同步,跨进程可以用于系统所有线程间同步。
作用域设置与获取函数
int pthread_condattr_setshared(pthread_condattr_t *attr,int pshared);
int pthread_condattr_getshared(pthread_condattr_t *attr,int *pshared);
attr
,条件变量属性实例地址,不能为NULLpshared
,作用域类型,PTHREAD_PROCESS_PRIVATE
和PTHREAD_PROCESS_SHARED
注:
条件变量和结合使用的互斥锁作用域要保持一致,更改也应同步更改。
条件变量时钟类型用于指定超时时间等待条件变量函数pthread_cond_timewait
超时参数的计算时钟。
时钟类型 | 描述 |
---|---|
CLOCK_REALTIME | 可设置的系统实时时钟 |
CLOCK_MONOTONIC | 不可设置的系统实时时钟 |
CLOCK_PROCESS_CPUTIME_ID | 进程cpu时间 |
CLOCK_THREAD_CPUTIME_ID | 线程cpu时间 |
时钟类型设置和获取函数
int pthread_condattr_setclock(pthread_condattr_t *attr,clockid_t clock_id);
int pthread_condattr_getclock(const pthread_condattr_t *attr,clockid_t *clock_id);
attr
,条件变量属性实例地址,不能为NULLclock_id
,时钟类型,可设置/获取值为上表宏条件变量是线程间同步的一种机制,条件变量往往是和互斥锁结合一起使用的。因此,使用过程,同样需关注死锁问题,要以条件变量和互斥锁的使用原则为基准。条件变量使用原则参考2.3节,互斥锁使用原则参数上一篇文章的2.3节。