线程的同步
在线程中如果有一个资源他是有限的,比如超市里的鸡腿,卖光了就没了,必须要进货才能买,但是进货的途中你不能拿,中途拿了那不叫买,叫抢劫。
所以在进货途中需要有人保护啊,这个时候互斥锁又来当保镖了,我进货的时候都老实点,全都不许动,要买等着,等我进了货你们再买,没了就等下一批鸡腿。
上面那个例子就是线程同步,消费者只能等生产者生产了才能消费,必须要有一个时序性。加入消费者凭空买了一个鸡腿,鸡腿数量变-1了,然后生产者生产了一个鸡腿把数量变成0,这就不合理了,所以时序性是必须的。
条件变量
条件变量能够实现线程同步,主要是管理以下的情况:
- 当某一个线程拿到资源访问的权限的时候,发现他虽然进来了,但是里面没有资源,在其他线程进行操作之前,他什么也做不了,所以只能归还资源访问权限,暂时挂起等待。
- 许多线程都拿到这个资源访问权限了,他们都和第一点中的线程一样,全都等待,等待也有先后顺序,需要一个队列,也由条件变量维护。
条件变量配合互斥锁
条件变量需要互斥锁配合使用,为什么需要配合互斥锁?说白了就是一句话,保证临界资源的安全性。
在最开始的卖鸡腿的例子里,互斥锁担任的是保镖的任务,其实就是这么个道理,在进货的时候不能被其他人获取资源,要保证数据的安全性,这和线程互斥的时候道理是一样的,资源被修改的时候需要保证资源的唯一访问性,因此这个工作就交给互斥锁来完成。
同时,这里说一下条件变量的wait
函数,wait函数在等待的同时还有一个解锁操作,为什么有这个解锁操作?
因为wait都是需要获取资源的线程调用的,如果没有资源,那么就会陷入等待,等待其他线程生产资源,这个时候如果不把互斥锁释放,就会死锁,所以需要在调用wait时候就给解锁了,但是还有一个问题,为什么不像下面这么做,也可以实现,但是却不这么做?
//这段代码是错误的
pthread_mutex_unlock(&mutex);//先解锁
pthread_cond_wait(&cond);//然后挂起等待
代码的这种思路是错误的,因为等待之后再执行解锁操作这个操作过程不是原子操作,可能在解锁之后,挂起等待之前,资源已经被增加,鸡腿在解锁之后就可以买了,但是这个线程不知道,如果不明白,看下面的例子:
就好像我要买一个鸡腿,结果超市告诉我没有了,于是我离开了货柜,准备抽根烟等补货,结果我点烟的时候补货了(点烟的时候我还没开始等,算是我从解锁状态到等待状态的间隙),然后超市说有货了,我没注意到,然后一直在抽烟一直抽烟,等半天等不到鸡腿。
所以我们需要解锁和挂起等待这个过程是一个原子操作,大佬们把这个挂起等待和解锁封装到一个函数内,做了一些操作让其变成原子操作,方便我们使用,不会出现上面的情况。
条件变量接口
线程同步借助条件变量来实现,下面介绍条件变量的一些接口:
- 初始化条件变量
两种初始方式:一用函数,需要释放,二赋值初始化,不需要释放
//函数初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
//cond:要初始化的条件变量
//attr:给NULL就行
//返回值:成功 0 失败 errno
//赋值初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- 条件变量的等待
int pthread_cond_destroy(pthread_cond_t *cond);
//cond:需要销毁的条件变量
//返回值:成功 0 失败 errno
- 条件变量的等待
除了普通的阻塞等待还有限时等待,如果超时就停止等待
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
//cond:操作的条件变量
//mutex:辅助的互斥锁
//返回值:成功 0 错误 errno
- 唤醒等待条件变量的线程
//唤醒所有等待条件变量的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
//唤醒第一个等待的线程
int pthread_cond_signal(pthread_cond_t *cond);
//cond:操作的条件变量
//返回值:成功 0 失败 errno
卖鸡腿代码
//使用pthread_cond和pthread_mutex实现同步
//生产者生产鸡腿,消费者买鸡腿
//生产者有生产计划,生产完了就不卖了
//消费者没鸡腿买了很伤心
//程序运行结束,干掉消费者和生产者
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#include <errno.h>
pthread_mutex_t mutex;
pthread_cond_t cond;
//1. 定义条件变量
// 条件变量的初始化有两种方式
// 1. 定义赋值初始化,不需释放
// 2. 函数接口初始化, 需要释放
// pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int chicken_leg = 0;
int production_plan = 10;
void* produce()
{
while (1)
{
//给生产者加互斥锁,没鸡腿的时候防止消费者进来买
pthread_mutex_lock(&mutex);
chicken_leg++;
production_plan--;
printf("Produce chicken leg~~\n");
if (production_plan == 0)
{
//完成生产计划,休息,解锁,别忘了,不然死锁
printf("All purduction plan complete, have a rest~~\n");
pthread_mutex_unlock(&mutex);
//解锁,并告诉生产者生产完毕
pthread_cond_broadcast(&cond);
break;
}
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond);
//int pthread_cond_broadcast(pthread_cond_t *cond);
// 唤醒所有等待在条件变量上的线程
//int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒第一个等待在条件变量上的线程
sleep(2);
}
return NULL;
}
void* consume()
{
//没有鸡腿就要等待
//int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
// pthread_cond_wait就是用来睡眠的
// 条件变量是和互斥锁搭配使用的
// 是先对互斥锁做了一个判断是否加锁,如果加锁了就解锁
// 然后陷入等待*******整个过程是原子操作
//
// 要防止的情况就是:假如没有鸡腿,但是消费者又速度比较快,
// 先拿到锁了,那么生产者将拿不到锁,没法生产将会造成双方卡死
// 所以如果消费者如果先获取到锁,那么在陷入等待之前需要解锁
// 而这里的锁的存在是为了保护这个全局的条件的操作是受保护的。(basket)
while (1)
{
//如果消费者进来,先加锁
pthread_mutex_lock(&mutex);
if (chicken_leg == 0)
{
//没鸡腿卖,那让消费者先等着,并解锁
pthread_cond_wait(&cond, &mutex);
}
printf("Producer buy a chicken leg~~\n");
chicken_leg--;
pthread_mutex_unlock(&mutex);
if (production_plan == 0)
{
//鸡腿卖完了,伤心
printf("No more chciken leg to buy -_-||, sad-----\n");
break;
}
sleep(1);
}
return NULL;
}
int main()
{
pthread_t consumer, producer;
int ret;
//初始化条件变量和互斥锁
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
ret = pthread_create(&producer, NULL, produce, NULL);
if (ret < 0)
{
perror("Pthread_create failed");
return -1;
}
ret = pthread_create(&consumer, NULL, consume, NULL);
if (ret < 0)
{
perror("Pthread_create failed");
return -1;
}
pthread_join(producer, NULL);
pthread_join(consumer, NULL);
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}
执行结果