线程同步(2)-- 条件变量

目录

12.1 条件变量

12.2 虚假唤醒

12.3 唤醒丢失

12.4 栗子




12.1 条件变量

条件变量给线程提供了一个汇聚的场所,条件变量已经不是锁的范畴了。
条件变量用于自动阻塞一个线程,直到有特殊情况发生。(生产者消费者模型)
通常情况下条件变量和互斥量共同使用。
注(1)

条件变量原语:

//初始化条件变量:
//本人还是喜欢静态初始化,省事儿
pthread_cont_t cont = PTHREAD_COND_INITIALIZER;

//好,再看看动态初始化
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
//参数释义:cond:用于接收初始化成功管道条件变量
//attr:通常为NULL,且被忽略

//有初始化那肯定得有销毁
int pthread_cond_destroy(pthread_cond_t *cond);

//既然说条件变量是用来等待的,那就更要看看这等待的特殊之处了
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);  //无条件等待
//注(2)这背后的故事需要慢慢道来
int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t mytex,const struct timespec *abstime);  //计时等待
//注(3)这里给出timeout的结构体

//好,加入等待唤醒大军了,那得看看怎么去唤醒了
int pthread_cond_signal(pthread_cond_t *cptr); //唤醒一个等待该条件的线程。存在多个线程是按照其队列入队顺序唤醒其中一个
int pthread_cond_broadcast(pthread_cond_t * cptr); //广播,唤醒所哟与等待线程
//注(4)


套路送上:

条件变量的使用可以分为两部分:

等待线程
使用pthread_cond_wait前要先加锁;
pthread_cond_wait内部会解锁,然后等待条件变量被其它线程激活;
pthread_cond_wait被激活后会再自动加锁;

激活线程:
加锁(和等待线程用同一个锁);
pthread_cond_signal发送信号;
解锁;


12.2 虚假唤醒

在多核处理器下,pthread_cond_signal可能会激活多于一个线程(阻塞在条件变量上的线程)。结果是,当一个线程调用pthread_cond_signal()后,多个调用pthread_cond_wait()或pthread_cond_timedwait()的线程返回。这种效应成为”虚假唤醒”(spurious wakeup)

Linux帮助里面有
为什么不去修正,性价比不高嘛。

所以通常的标准解决办法是这样的:

在这里插入图片描述
什么情况下会发生虚假唤醒?
只有在生产线程与消费线程都为多个的情况下才有可能发生虚假唤醒。

12.3 唤醒丢失

唤醒丢失的概念在注(2)里面了。

在线程未获得相应的互斥锁时调用pthread_cond_signal或pthread_cond_broadcast函数可能会引起唤醒丢失问题。

唤醒丢失往往会在下面的情况下发生:

一个线程调用pthread_cond_signal或pthread_cond_broadcast函数;
另一个线程正处在测试条件变量和调用pthread_cond_wait函数之间;
没有线程正在处在阻塞等待的状态下。


12.4 栗子
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>

struct msg 
{
	struct msg *next;
	int num;
};

struct msg *head;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void *costomer(void *arg)
{
	struct msg *temp;
	//套路开始

	pthread_mutex_lock(&mutex);  //先锁上
	while( head == NULL )  //防止虚假唤醒
		pthread_cont_wait();  //好,阻塞等待唤醒,在这里先把锁解开了,出去的时候再锁上
	temp = head;  //唤醒之后开始干活
	head = temp->next;
	
	pthread_mutex_unlock(&mutex);  //好,解锁意味着干完活了
	
	printf("Customer:%d",temp->num);
	free(temp); //做事儿得有始有终
	sleep(5);
}


void *produce( void *arg )
{
	struct *msg temp;
	static int i = 0;

	for(;i<10;i++)
	{
		temp = malloc(sizeof(struct msg));
		memset(temp,0,sizeof(struct msg));
		temp->num = i;
		printf("Produce:%d",temp->num);
		
		pthread_mutex_lock(&mutex);	//锁上,干活
		temp->next = head;
		head = temp;
		pthread_mutex_unlock(&mutex);	//完事儿,解锁

		pthread_cond_signal(&cond);	//喊一个过来收产品
		
		if(i == 10)
		{
			i = 0;
		}
	
		sleep(5);
	}
}


int main(/*.......*/)
{
	pthread_t pid1,pid2;	//来俩线程试一试
	
	pthread_create(&pid1,NULL,produce,NULL);	//这个是生产者
	pthread_create(&pid2,NULL,costomer,NULL);	//这个是消费者

	//等着回收
	pthread_join(pid1);
	pthread_join(pid2);

	return 0;
}
  • 写在最后:

注(1):

在服务器编程中常用的线程池,多个线程会操作同一个任务队列,一旦发现任务队列中有新的任务,子线程将取出任务;这里因为是多线程操作,必然会涉及到用互斥锁保护任务队列的情况(否则其中一个线程操作了任务队列,取出线程到一半时,线程切换又取出相同任务)。但是互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。设想,每个线程为了获取新的任务不断得进行这样的操作:锁定任务队列,检查任务队列是否有新的任务,取得新的任务(有新的任务)或不做任何操作(无新的任务),释放锁,这将是很消耗资源的。

而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起配合使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线程间的同步。对应于线程池的场景,我们可以让线程处于等待状态,当主线程将新的任务放入工作队列时,发出通知(其中一个或多个),得到通知的线程重新获得锁,取得任务,执行相关操作。

注(2):

无论哪种等待方式,都必须和一个互斥量配合,以防止多个线程来打扰。
互斥锁必须是普通锁或适应锁,并且在进入pthread_cond_wait之前必须由本线程加锁。
在更新等待队列前,mutex必须保持锁定状态.
在线程进入挂起,进入等待前,解锁。(好绕啊,我已经尽力断句了)
在条件满足并离开pthread_cond_wait前,上锁。以恢复它进入cont_wait之前的状态。

为什么等待会被上锁?

以免出现唤醒丢失问题。
这里有个大神解释要不要看:https://stackoverflow.com/questions/4544234/calling-pthread-cond-signal-without-locking-mutex
做事做全套,源码也给放这儿了:https://code.woboq.org/userspace/glibc/nptl/pthread_cond_wait.c.html
在放些咱能看懂的中文解释:将线程加入唤醒队列后方可解锁。保证了线程在陷入wait后至被加入唤醒队列这段时间内是原子的。
但这种原子性依赖一个前提条件:唤醒者在调用pthread_cond_broadcast或pthread_cond_signal唤醒等待者之前也必须对相同的mutex加锁。
满足上述条件后,如果一个等待事件A发生在唤醒事件B之前,那么A也同样在B之前获得了mutex,那A在被加入唤醒队列之前B都无法进入唤醒调用,因此保证了B一定能够唤醒A;试想,如果A、B之间没有mutex来同步,虽然B在A之后发生,但是可能B唤醒时A尚未被加入到唤醒队列,这便是所谓的唤醒丢失。

注(3):

struct timespec
{
    time_t tv_sec;        /* Seconds. */
    long   tv_nsec;       /* Nanoseconds. */
};

//Linux中常用的时间结构有struct timespec 和 struct timeval 
//顺便把timeval的结构体也放这里

struct timeval {
        time_t tv_sec;  
        suseconds_t tv_usec;
}; 

//一个是带纳秒,一个是带微秒的

注(4):

(1)必须在互斥锁的保护下唤醒,否则唤醒可能发生在锁定条件变量之前,照成死锁。
(2)唤醒阻塞在条件变量上的所有线程的顺序由调度策略决定
(3)如果没有线程被阻塞在调度队列上,那么唤醒将没有作用。
(4)以前不懂事儿,就喜欢广播。由于pthread_cond_broadcast函数唤醒所有阻塞在某个条件变量上的线程,这些线程被唤醒后将再次竞争相应的互斥锁,所以必须小心使用pthread_cond_broadcast函数。

发布了61 篇原创文章 · 获赞 3 · 访问量 1629

猜你喜欢

转载自blog.csdn.net/qq_43762191/article/details/103965895