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