Linux基础——线程同步之条件变量

1、条件变量

条件变量必须配合着互斥锁使用,互斥锁查看-》线程同步(互斥锁)

1.1、条件变量概念

  • 条件变量是什么:
    本身不是锁,满足某个条件,像加锁一样,造成阻塞,与互斥量配合,给多线程提供会所。

  • 为什么要用条件变量:
    在线程抢占互斥锁时,线程A抢到了互斥锁,但是条件不满足,线程A就会让出互斥锁让给其他线程,然后等待其他线程唤醒他;一旦条件满足,线程就可以被唤醒,并且拿互斥锁去访问共享区。经过这中设计能让进程运行更稳定。

1.2、条件变量控制原语

pthread_cond_t 			用于创建条件变量
pthread_cond_init 			用于初始化条件变量
pthread_cond_destroy 		用于销毁条件变量
pthread_cond_wait 			用于阻塞条件变量
pthread_cond_timedwait 	用于计时阻塞条件变量
pthread_cond_signal 		用于唤醒条件变量
pthread_cond_broadcast		用于广播所有条件变量

1.2.1、条件变量——创建

条件变量的创建和互斥锁一样,拥有两种创建方式:静态创建、动态创建

  • 静态创建:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER; 

注意:PTHREAD_COND_INITIALIZER是一个常量

  • 动态创建
    • 函数:int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
    • 参数1:传入一个条件变量
    • 参数2:属性设置,一般都是写NULL
    • 返回值:成功返回0,失败返回错误码。
    • 使用案例:pthread_cond_init(cond, NULL);

1.2.2、条件变量——销毁

  • 函数:int pthread_cond_destroy(pthread_cond_t *cond) ;
  • 参数1:传入一个条件变量
  • 返回值:成功返回0,失败返回错误码
  • 注意:要在使用之前,你要先确保没有线程在等待,不然会返回EBUSY。

1.2.3、条件变量——等待

  • 普通等待
    • 函数:int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
    • 参数1:传入一个条件变量
    • 参数2:传入一个互斥锁
    • 作用:将互斥锁让给其他线程使用,如果被唤醒就去拿互斥锁(如果互斥锁有其他线程再用就阻塞)
  • 计时等待(对我个人来说,我没怎么用到这个)
    • 函数:int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,const struct timespec *abstime);
    • 参数1:传入一个条件变量
    • 参数2:传入一个互斥锁
    • 函数3:传入struct timespec 类型的时间结构体
    • 返回值:成功返回0,失败返回错误码
    • 作用:在指定的时间内,这函数的作用和pthread_cond_wait函数一样,只是说过了这个时间,还没被唤醒就返回一个ETIMEOUT,并且让互斥锁再解锁,让其他线程抢占。

1.2.4、条件变量——唤醒

在代码中,唤醒和等待一般是配套使用的,就好比互斥锁中的拿锁和解锁的关系。

  • 唤醒至少一个线程的条件变量

    • 函数:int pthread_cond_signal(pthread_cond_t *cptr);
    • 参数1:传入一个条件变量
    • 返回值:成功返回0,失败返回错误码。
  • 广播(唤醒全部线程的条件变量)

    • 函数:int pthread_cond_broadcast (pthread_cond_t * cptr);
    • 参数1:传入一个条件变量
    • 返回值:成功返回0,失败返回错误码。

1.3、示例代码——生产者消费者模型

在这示例代码会建立一个对链表操作的生产者消费者模型。这一般只能有两个线程,如果需要多个线程参加,可以看后面的线程同步之信号量,生产者消费者模型如下图所示:
在这里插入图片描述

#include <stdlib.h> 
#include <pthread.h> 
#include <stdio.h>
//定义链表的节点
struct msg 
{ 
	struct msg *next; 
	int num; 
};

//建立全局的一个头结点
struct msg *head;

//静态建立一个条件变量has_product,放在全局里
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER; 

//静态建立一个互斥锁lock,放在全局里
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

//声明一下两个执行函数
void *producer(void *p)void *consumer(void *p)//主函数
int main(void) 
{ 
	pthread_t pid, cid;
	
	//建立一个随机种子
	srand(time(NULL)); 
	
	//创建生产者线程和消费者线程
	pthread_create(&pid, NULL, producer, NULL); 	
	pthread_create(&cid, NULL, consumer, NULL); 
	
	//回收两个线程
	pthread_join(pid, NULL); 
	pthread_join(cid, NULL); 
	return 0;
}

void *consumer(void *p) 
{ 
	//建立一个临时的链表的游标mp
	struct msg *mp;
	for (;;) 
	{ 
		pthread_mutex_lock(&lock); 
		while (head == NULL)
		{
			//消费者线程拿到互斥锁后等待,并且把互斥锁让出来。
		 	pthread_cond_wait(&has_product, &lock); 
		 }
		 
		 //链表移动
		 mp = head; 
		 head = mp->next; pthread_mutex_unlock(&lock); 
		 //打印当前节点的num
		 printf("Consume %d\n", mp->num); 
		 //删除游标
		 free(mp); 
		 //随机延时
		 sleep(rand() % 5); 
	}
}

void *producer(void *p) 
{ 
	
	struct msg *mp; 
	for (;;) 
	{ 
		//给节点开辟空间
		mp = malloc(sizeof(struct msg));
		//随机赋值num 0-1000的值
		mp->num = rand() % 1000 + 1;
		printf("Produce %d\n", mp->num); 
		//给互斥锁加锁(防止其他线程抢占)
		pthread_mutex_lock(&lock); 
		//移动游标
		mp->next = head; 
		head = mp; 
		//互斥锁解锁,让其他线程抢占
		pthread_mutex_unlock(&lock); 
		//唤醒消费者线程
		pthread_cond_signal(&has_product); 
		//随机睡眠
		sleep(rand() % 5); 
	} 
}

2、易错点——曾遇到的问题和解决方法

这里我重点说明一下几点,我自己在做项目中出现的错误:下面是我写的一个例子,为了测试条件变量是否能正常使用。想得到的效果是:生产者先 i +1后,消费者再 i +1。

下面是我在写项目中第一次使用时的错误写法

按现在看来就是:没有给条件变量上条件

#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>

//条件变量和互斥锁的创建
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

int i=0;	//定义一个全局变量来当作线程的间的共享数据

//消费者线程
void *consumer(void *p)
{
	printf("消费者:消费者线程开始工作\n");
	for (;;) 
	{
		pthread_mutex_lock(&lock);	//拿锁
		pthread_cond_wait(&has_product, &lock);	//这里我没有使用条件锁住条件变量,使用条件变量的等待函数,把锁让出去给其他线程
		printf("消费者:i = %d \n",i);
		i++;
		pthread_mutex_unlock(&lock);	//解锁
		if(i>=15)			//这里为了方便查看成果,我们将i超过一定数额后退出循环
		{
			break;
		}
	}
}
//生产者线程
void *producer(void *p)
{
	printf("生产者:生产者线程开始工作\n");
	for (;;) 
	{
		pthread_mutex_lock(&lock);		//拿锁
		printf("生产者:i = %d \n",i);
		i++;
		pthread_mutex_unlock(&lock);		//解锁
		pthread_cond_signal(&has_product);	//唤醒消费者
		if(i>=15) //这里为了方便查看成果,我们将i超过一定数额后退出循环
		{
			break;
		}
	}
}

//主函数
int main(int argc, char *argv[])
{
	pthread_t pid, cid;				//创建两个线程
	pthread_create(&pid, NULL, producer, NULL); 	//生产者线程,这里请注意,生产者线程在前,消费者在后
	pthread_create(&cid, NULL, consumer, NULL);	//消费者线程
	pthread_join(pid, NULL);			
	pthread_join(cid, NULL);
	while(1);
    	return 0;
}	

2.1、第1次修改代码

2.1.1、思路

在这代码中,我当时的思路是,生产者生产出了一个东西出来,唤醒消费者去工作。消费者拿到锁之后,把锁让出来,被唤醒后出来工作。这感觉逻辑没什么问题,但是我编译出来是这样的……在这里插入图片描述

2.1.2、解析错误点:

这很显然不是我想要的效果,生产者一直工作,唤醒消费者无效。这里我认为是,电脑跑太快,导致消费者线程还没被创建出来,生产者就跑完了

2.1.3、错误点第一次改进:

  • 我在每个if(i>=15)前加了个sleep(1),让线程先休眠一秒,避免说是生产者线程跑太快了,没机会给消费者线程执行。
  • 在main函数中,先创建消费线程,再创建生产者线程,理由是可以让消费者线程先阻塞等待生产者。

然后执行结果如下:
在这里插入图片描述

2.2、第2次修改代码

2.2.1、解析不足:

这现在是达到了我想要的结果,但是在一个服务器中,各种资源都是十分有限的,不可能说是使用sleep来让线程慢下来。

2.2.2、错误点第二次改进:

  • 我把if(i>=15)这个去掉,让服务器一直跑下去,来模拟现实情况。
  • 去掉sleep函数,防止资源占用。

最终执行效果如下
在这里插入图片描述

2.3、第3次修改代码

2.3.1、解析不足:

在这结果中,生产者还是跑飞了,我在一堆生产者中才找到一个消费者。我想就是消费者每次都是拿到锁就让出去了,那么这个条件变量(条件变量的等待函数pthread_cond_wait)就压根没起到作用。

2.3.2、错误点第三次改进:

  • 根据老师讲过的知识点(见文章开头的第1大点部分),给条件变量上锁。
  • 再根据上文1.3示例代码的不足进行修改(示例代码中,数据都是先进后出),使用环形队列(让数据可以先进先出)来优化代码

2.3.3、改进后的代码

#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
#define BUF_SIZE 10  //环形队列实际只能存 BUF_SIZE - 1 个

//环形队列
typedef struct Queue
{
	int * BUF; //队列的数据
	int front; //队列的头指针
	int rear; //队列的尾指针
}QUEUE_t;

QUEUE_t my_queue; //创建一个全局的环形队列
int i=0;   //用于测试
int flag=0;

//信号量和和互斥量的静态创建
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

//环形队列的初始化
void Queue_init(QUEUE_t *queue_q)
{
	queue_q->BUF = (int *)malloc(sizeof(int)*BUF_SIZE);
	if(queue_q->BUF != NULL)
	{
		queue_q->front = queue_q->rear = 0;  //初始化头尾指针的位置
	}
}

//判断环形队列是否为空
int Queue_if_empty(QUEUE_t *queue_q)
{
	if(queue_q->front == queue_q->rear)
	{
		return 1;
	}
	else
 	{
        	return 0;
 	}
}

//判断环形队列是否为满
int Queue_if_full(QUEUE_t *queue_q)
{
 	if((queue_q->rear +1) % BUF_SIZE == queue_q->front)
    	{
        	return 1;
    	}
	else
 	{
        	return 0;
 	}
}

//环形队列添加数据
void Queue_Add(QUEUE_t *queue_q , int value)
{
    	if(Queue_if_full(queue_q) != 1)        //队列有空位
    	{
        	queue_q->BUF[queue_q->rear] = value;
        	queue_q->rear = (queue_q->rear + 1)%BUF_SIZE ;    
    	}
}

//环形队列输出数据
void Queue_Out(QUEUE_t *queue_q , int *value)
{
    	if(Queue_if_empty(queue_q) != 1)        //队列有数据
    	{
        	*value = queue_q->BUF[queue_q->front];
        	queue_q->front = (queue_q->front + 1)%BUF_SIZE ;
    	}
}

//消费者线程
void *consumer(void *p)
{
    	int value;
    	for (;;) 
    	{
        	pthread_mutex_lock(&lock);//拿锁
        	while (Queue_if_empty(&my_queue) == 1) //环形队列为空
  		{
            		pthread_cond_wait(&has_product, &lock); //让出互斥锁
  		}
  		Queue_Out(&my_queue, &value);
  		printf("消费者:提取的数据为 i = %d\n",value); 
        	pthread_mutex_unlock(&lock);
  		if(flag ==1)
  		{
   			flag = 0;
   			pthread_cond_signal(&has_product);
  		}
  		if(i>=100000)
  		{
   			break;
  		}
    	}
}

//生产者线程
void *producer(void *p)
{
    	for (;;) 
    	{
        	pthread_mutex_lock(&lock);
  		while (Queue_if_full(&my_queue) == 1) //环形队列为满
 	 	{
   			flag = 1;
            		pthread_cond_wait(&has_product, &lock); //让出互斥锁
  		}
  
  		//生产一个产品
  		Queue_Add(&my_queue,++i);
  		printf("生产者:存入数据为 i = %d\n",i); 
        	pthread_mutex_unlock(&lock);
  		//唤醒阻塞在条件变量上的吃货`
  		pthread_cond_signal(&has_product);
  		if(i>=100000)
  		{
   			break;
  		}
    	}
}
int main(int argc, char *argv[])
{
    	pthread_t pid, cid;
 	//初始化环形队列
 	Queue_init(&my_queue);
    	pthread_create(&cid, NULL, consumer, NULL);
 	pthread_create(&pid, NULL, producer, NULL);
 	
    	//pthread_join(pid, NULL);
 	//printf("回收了生产者\n");
    	//pthread_join(cid, NULL);
 	//printf("回收了消费者\n");
 	while(1);
    	return 0;
}

2.3.4、执行完后的效果为下图,完成了线程间的通信!

在这里插入图片描述

2.3.5、改进部分的解析

由于这最后一次更改的代码量比较多,我这里总结一下主要更改内容和期间遇到的问题及解决方法:
主要更改内容:使用环形队列、给条件变量加上条件。

先说一下环形队列方面

  • 环形队列注意点:
    1、环形队列满的时候,生产要不要继续生产?
    2、环形队列为空的时候,消费者还能不能消费?
  • 环形队列的处理方案
    1、队列满的时候,生产者要适当停下来,给消费者一些时间去消费。这里我们可以通过提高环形队列的内存大小来降低这种情况的出现。(在第三次改进代码的第四行宏定义BUF_SIZE,来更改环形队列大小)
    2、环形队列空的时候,消费者要等一下生产者生产。

再说一下条件变量方面的注意点

  • 生产者和消费者都使用条件变量来限制执行
  • 主要是当环形队列满的时候,生产者阻塞等待,这是时候,我们就需要消费者把队列里面的产品消费掉一些后再去唤醒生产者。当然这只是我个人的一个处理方法,如果你们有其他更优的方法也可以试着更改使用。

猜你喜欢

转载自blog.csdn.net/l1206715877/article/details/106877093