【Linux应用编程】POSIX线程互斥与同步机制—条件变量

文章目录

  • 1 前言
  • 2 条件变量
    • 2.1 条件变量特点
    • 2.2 条件变量适用场景
    • 2.3 条件变量使用原则
  • 3 条件变量使用
    • 3.1 创建条件变量
    • 3.2 初始化条件变量
    • 3.3 条件变量获取(等待)
      • 3.3.1 阻塞方式
      • 3.3.2 指定超时时间阻塞方式
      • 3.3.3 与互斥锁结合使用
    • 3.4 条件信号产生
      • 3.4.1 点对点方式
      • 3.4.2 广播方式
      • 3.4.3 与互斥锁结合使用
    • 3.5 条件变量销毁
    • 3. 6 写个例子
  • 4 条件变量属性
    • 4.1 创建条件变量属性
    • 4.2 条件变量属性初始化与销毁
    • 4.3 条件变量作用域
    • 4.4 条件变量时钟类型
  • 5 总结


1 前言

  在上一篇文章中,主要描述了互斥锁的含义、属性、使用原则以及使用方法。互斥锁是有个不足就是只能用于线程间的互斥,不能用于“同步”,因此互斥锁往往不是独立使用的。本文描述条件变量的相关内容,因为条件变量往往是和互斥锁一起结合使用的,为什么会这样使用,详见下面章节。


2 条件变量

  条件变量(Condition variable)是线程间同步的一种机制。条件变量本质是一个信号量,一个线程阻塞等待某一条件,如果未获取的条件变量信号一直阻塞,直至获取到;另一个线程在达到预期条件时,产生一个通知信号给等待线程,等待线程被唤醒继续执行,达到同步的目的。


2.1 条件变量特点

  • 用于线程同步
  • 可引起线程睡眠,没有等待到条件变量信号,线程进入睡眠状态

2.2 条件变量适用场景

  • 生产者消费者模型

2.3 条件变量使用原则

  • 条件变量和互斥锁一起使用
  • 互斥锁使用普通锁,不能使用嵌套锁
  • 条件变量不允许复制,但可以复制其指针
  • 条件变量和谓语关系一般是1:1或者1:N;尽量避免N:1,容易造成死锁
  • 等待线程获取到条件变量,需再次手动检查谓语否成立,特别是1:N的情况

【1】 唤醒可能被拦截,多个线程等待条件变量时,由于系统的并发性,在线程A获取到条件变量前,线程B已获取到并重置了共享数据的状态;由于谓语已经不成立,线程A如果不检查谓语可能会导致不可预期的后果(访问空指针等)。

【2】增加程序健壮性

条件变量谓语:

谓语表示具有唯一真假值的语句,程序中可以用谓语来描述当前线程需要的条件状态。条件变量的关注对象是共享数据状态的变化,这一变化可以使用谓语来描述。 如果谓语为假,则等待条件变量信号;获取到条件变量后,再一次判断谓语真假,来决定是否执行任务。


3 条件变量使用

  条件变量使用的基本步骤为:

【1】创建条件变量实例

【2】初始化条件变量

【3】等待条件变量

【4】发送条件变量信号

【5】销毁条件变量


3.1 创建条件变量

  posix线程互斥锁以pthread_cond_t数据结构表示。条件变量实例可以用静态和动态创建。

pthread_cond_t cond;

3.2 初始化条件变量

  条件变量初始化可以使用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;

3.3 条件变量获取(等待)

  条件变量获取分为无限阻塞方式和指定超时时间阻塞方式。

3.3.1 阻塞方式

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
  • cond,条件变量实例地址,不能为NULL
  • mutex,互斥锁实例地址,不能为NULL
  • 返回,成功返回0,参数无效返回EINAL

3.3.2 指定超时时间阻塞方式

int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,
                           const struct timespec *abstime);
  • cond,条件变量实例地址,不能为NULL
  • mutex,互斥锁实例地址,不能为NULL
  • abstime,超时时间,单位为时钟节拍
  • 返回
返回值 描述
0 成功
EINVAL 参数无效
EDEADLK 互斥锁为非嵌套锁重复调用该函数
EBUSY 锁被其他线程持有
ETIMEDOUT 超时

  该函数不会无限等待,超出指定时间节拍后,仍未等待到条件变量信号函数会返回,返回ETIMEDOUT


3.3.3 与互斥锁结合使用

  条件变量等待必须与互斥锁一起使用,因为系统的并发性,多个线程在等待获取条件变量时发生竞态。也可以这样理解,条件变量信号也是共享资源,需要互斥访问。互斥量等待参考伪代码:

pthread_mutex_lock(&mutex)
while(/* 谓语 */)
{
	pthread_cond_wait(&cond, &mutex);
}
if (/* 谓语成立 */)
{
	/* todo */
}
pthread_mutex_unlock(&mutex);

3.4 条件信号产生

  条件信号产生分为点对点方式和广播方式,点对点方式只能唤醒一个等待条件变量的线程,广播方式可以唤醒所有等待条件变量的线程。


3.4.1 点对点方式

int pthread_cond_signal(pthread_cond_t *cond);
  • cond,条件变量实例地址,不能为NULL
  • 返回,成功返回0,参数无效返回EINAL

3.4.2 广播方式

int pthread_cond_broadcast(pthread_cond_t *cond);
  • cond,条件变量实例地址,不能为NULL
  • 返回,成功返回0,参数无效返回EINAL

3.4.3 与互斥锁结合使用

  条件变量信号产生前,对共享资源的访问,依然需要加锁处理,保护共享资源。典型使用参考伪代码:

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);

  • 在锁之间

优点:无线程优先级翻转问题
缺点: 潜在的性能损耗问题,降低效率

  一般情况下,推荐使用第二种方式。


3.5 条件变量销毁

int pthread_cond_destroy(pthread_cond_t *cond);
  • cond,条件变量实例地址,不能为NULL
  • 返回
返回值 描述
0 成功
EINVAL cond已被销毁过,或者cond为空
EBUSY 条件变量被其他线程使用

  pthread_cond_destroy用于销毁一个已经使用动态初始化的条件变量。销毁后的条件变量处于未初始化状态,条件变量的属性和控制块参数处于不可用状态。使用销毁函数需要注意几点:

  • 已销毁的条件变量,可以使用pthread_cond_init重新初始化使用
  • 不能重复销毁已销毁的条件变量
  • 使用宏PTHREAD_COND_INITIALIZER静态初始化的互斥锁不能销毁
  • 没有线程使用条件变量时(阻塞线程、信号产生),才能销毁

3. 6 写个例子

  代码实现功能:

  • 创建一个“生产者——消费者”模型
  • 创建两个线程,一个线程负责产生数据,另一个读取数据并输出到终端
  • 两个线程共享一个内存,通过条件变量同步
#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函数加锁,实际使用是不允许的,违反了加锁的原则,这里只是模拟场景测试。


4 条件变量属性

  使用默认的条件变量属性可以满足绝大部分的应用场景,特殊场景也可以调整条件变量属性。下面描述主要的条件变量属性及API。条件变量属性设置,基本步骤为:

【1】创建条件变量属性实例

【2】初始化属性实例

【3】设置属性

【4】销毁属性实例


4.1 创建条件变量属性

  posix线程条件变量属性以pthread_condattr_t 数据结构表示。条件变量属性实例可以用静态和动态创建。

pthread_condattr_t attr;

4.2 条件变量属性初始化与销毁

int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
  • attr,条件变量属性实例地址,不能为NULL
  • 成功返回0,参数无效返回 EINVAL

  设置条件变量属性时,首先创建一个属性pthread_condattr_t实例,然后调用pthread_condattr_init函数初始实例,接下来就是属性设置。初始化后的属性值就是默认条件变量属性,等价于使用pthread_cond_init采用默认属性(attr传入NULL)的初始化。


4.3 条件变量作用域

  条件变量作用域表示条件变量的作用范围,分为进程内(创建者)作用域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,条件变量属性实例地址,不能为NULL
  • pshared,作用域类型,PTHREAD_PROCESS_PRIVATEPTHREAD_PROCESS_SHARED
  • 成功返回0,参数无效返回 EINVAL

注:
条件变量和结合使用的互斥锁作用域要保持一致,更改也应同步更改。


4.4 条件变量时钟类型

  条件变量时钟类型用于指定超时时间等待条件变量函数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,条件变量属性实例地址,不能为NULL
  • clock_id,时钟类型,可设置/获取值为上表宏
  • 返回,成功返回0

5 总结

  条件变量是线程间同步的一种机制,条件变量往往是和互斥锁结合一起使用的。因此,使用过程,同样需关注死锁问题,要以条件变量和互斥锁的使用原则为基准。条件变量使用原则参考2.3节,互斥锁使用原则参数上一篇文章的2.3节。

你可能感兴趣的:(Linux应用编程,#,POSIX线程)