目录
一、概念
二、互斥锁实现互斥
三、条件变量实现同步
银行家算法
生产者与消费者模型
一、概念
概念:在多线程程序中,如果涉及到了对共享资源的操作,则有可能会导致数据二义性,而线程安全就指的是,就算对共享资源进行操作也不会导致数据二义
实现:如何实现多线程中对共享资源的操作不会出问题
互斥:通过同一时间对资源访问的唯一性,保证访问安全
互斥的实现:互斥锁(读写锁、自旋锁...)
同步:通过条件控制,让多执行对资源的获取更加合理
同步实现:条件变量、信号量
二、互斥锁实现互斥
实现对共享资源的唯一访问
1)互斥锁实现互斥的原理
本质:就是一个1/0计数器,通过0/1标记资源的访问状态(0-不可访问、1-可访问)
在访问资源之前进行加锁(通过状态判断是否可访问,不可访问则阻塞)
在访问资源之后进行解锁(将资源状态置为可访问状态,唤醒其他阻塞的线程)
也有另一种理解:访问资源之前加锁(获取锁资源-获取不到就阻塞)
访问完毕解锁(归还锁资源)
多个线程想要实现互斥,就必须访问同一个锁才可以,也就意味着锁也是一个共享资源
互斥锁的操作本身必须是安全的:互斥锁本身计数器的操作时原子操作
2)互斥锁如何实现自身操作安全的原理
我们知道内存与cpu之间的数据传输:当进行加锁操作时,先将锁中的1置入cpu(先将变量数据从内存加载到cpu)然后才能从cpu中对数据进行处理(转化为0)然后在从cpu中将数据加载到内存指定位置(完成将1替换为0)
然而这样一种操作就会产生问题,如果1从内存加载到cpu还没有将处理后的0加载回内存时,这时切换其他线程运行访问到的锁资源仍然为1,就会继续进行加锁,这无疑是致命的。
所以对于锁资源本身来说,计数器的操作必须是原子性的才可以。
有个指令类似于exchange,功能是交换指定寄存器与内存中的数据
1、先将指定寄存器中的值修改为0,
2、将寄存器与内存中的数据进行互换
3、判断是否符合获取锁的条件或者说判断是否能够加锁
这样使用exchange指令就可以实现计数器操作为原子操作。
3)接口
互斥锁类型变量 pthread_mutex_t
初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr)
mutex:互斥锁变量的地址 attr:互斥锁变量属性(通常置NULL)
访问资源前加锁
int pthread_mutex_lock(pthread_mutex_t *mutex) 阻塞加锁(老实人)
int pthread_mutex_trylock(pthread_mutex_t *mutex) 非阻塞加锁(海王)
访问资源后解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex)
释放销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER (这种初始化不需要销毁)
4)代码模拟
当不使用互斥锁线程之间对同一变量的访问情况如何?
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
int ticket = 100;
void *Scalper(void *arg)
{
while(1)
{
if(ticket > 0)
{
usleep(10);
printf("%p:我抢到了第 %d 号票\n",pthread_self(), ticket);
ticket--;
}
else{
printf("%p:票完了,我的工作结束了\n", pthread_self());
break;
}
}
return NULL;
}
int main()
{
pthread_t tid[4];
for(int i = 0; i < 4; i++)
{
int ret = pthread_create(&tid[i], NULL, Scalper, NULL);
if(ret != 0){
perror("create error");
return -1;
}
}
for(int i = 0; i < 4; i++)
{
pthread_join(tid[i], NULL);
}
return 0;
}
发现出现了很多意外情况,重复、负数票号,
为什么呢?就是因为每个线程之间访问同一变量,一个线程还正对变量进行操作的时候另一个线程也进行操作,于是就发生了同一变量的多次出现。
使用互斥锁保护临界区
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
int ticket = 100;
void *Scalper(void *arg)
{
while(1)
{
pthread_mutex_lock(arg); // 访问之前加锁
if(ticket > 0)
{
usleep(10);
printf("%p:我抢到了第 %d 号票\n",pthread_self(), ticket);
ticket--;
pthread_mutex_unlock(arg); // 在所有有可能退出的地方解锁
}
else{
printf("%p:票完了,我的工作结束了\n", pthread_self());
pthread_mutex_unlock(arg); // 在所有有可能退出的地方解锁
break;
}
usleep(1);
}
return NULL;
}
int main()
{
pthread_mutex_t mutex; // 定义锁变量 mutex
pthread_mutex_init(&mutex, NULL); // 初始化锁资源
pthread_t tid[4];
for(int i = 0; i < 4; i++)
{
int ret = pthread_create(&tid[i], NULL, Scalper, &mutex);
// 注意锁资源通过线程创建第四个参数传入入口函数内
if(ret != 0){
perror("create error");
return -1;
}
}
for(int i = 0; i < 4; i++)
{
pthread_join(tid[i], NULL);
}
return 0;
}
5)死锁
死锁是一种状态,是一种因为资源争抢不当导致程序流程卡死无法继续向下推进的状态。
多个线程对锁资源的争抢使用不当导致程序流程卡死,无法继续向下推进的状态
1、加锁之后没有释放就退出,导致其他线程获取不到锁资源卡死
2、多锁使用时,加锁顺序不当,线程1加锁顺序为AB,线程2加锁顺序为BA
发生死锁的必要条件
① 互斥条件 同一时间一把锁只能被一个线程所获取到
② 不可剥夺条件 一个线程加的锁,只能自己释放,其他线程无法释放
③ 请求与保持 一个线程请求了A锁之后请求B锁,如果请求不到就不会释放A锁
④ 环路等待 线程1加了A锁后请求B锁,线程2加了B锁后请求A锁
死锁的预防:破坏死锁产生的必要条件
① 和 ② 无法修改
写代码时要注意:
1、线程之间的加解锁顺序尽量一致 -- 尽可能预防环路等待
2、采用非阻塞加锁,如果加不上锁,则把已经加上的锁释放 -- 破坏请求与保持
(请求不到新的,则释放已有的)
避免:银行家算法、死锁检测算法……
银行家算法http://t.csdn.cn/1YxZj
三、条件变量实现同步
1)概念
同步:通过条件控制,保证资源访问的合理性
条件变量:主要是一个pcb等待队列,以及唤醒和阻塞线程的接口
原理:
线程1 获取资源时进行判断,如果线程不符合资源获取条件,则调用阻塞接口进行阻塞
线程2 促使资源获取条件满足之后(生产资源),通过唤醒接口唤醒阻塞的线程
条件变量需要搭配互斥锁来使用
举例:
顾客 与 厨师 (消费者与生产者)
顾客来到柜台,看到柜台有饭则吃饭,否则阻塞
厨师来到柜台,看到柜台上没有饭则做饭,否则阻塞
顾客:
0、加锁(关门)
1、访问柜台有没有饭
有饭则吃饭,没有饭就阻塞
(这里阻塞时需要进行解锁,否则厨师就无法访问柜台,也就无法做饭,产生死锁)
阻塞则需先解锁,再阻塞,被唤醒之后再加锁
2、吃饭
3、吃完了,再来一碗 唤醒厨师
4、解锁
厨师:
0、加锁
1、访问柜台有没有饭
没饭则做饭,有饭则阻塞
(这里是有饭就需要解锁,让顾客能够吃饭,否则会死锁)
阻塞需先解锁,再阻塞,被唤醒后加锁
2、做饭
3、做好了,唤醒顾客
4、解锁
2)接口
定义条件变量:
pthread_cond_t 条件变量的变量类型
初始化条件变量:
pthread_cond_t cond_init (pthread_cond_t *cond, pthread_condattr_t *attr);
阻塞接口:条件变量是搭配互斥锁一起使用的,就体现在阻塞这一步
int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex) -- 阻塞接口
int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex,
struct timespec *t ) -- 有时长限制的阻塞
唤醒接口:
int pthread_cond_signal(pthread_cond_t *cond) 唤醒至少一个阻塞队列中的线程
int pthread_cond_broadcast(pthread_cond_t *cond) 唤醒等待队列中所有的线程
销毁接口:
int pthread_cond_destroy(pthread_cond_t *cond)
3)模拟实现
#include<stdio.h>
#include<pthread.h>
int counter = 0; // 定义柜台状态 0——没饭 1——有饭
pthread_mutex_t mutex; // 初始化锁
pthread_cond_t cond; // 初始化条件变量
void* customer(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex); // 先加锁
if(counter==0){
pthread_cond_wait(&cond, &mutex); // 没饭则解锁 并阻塞,等待唤醒,唤醒后加锁
}
printf("真好吃,再来一碗\n");
counter = 0;
pthread_cond_signal(&cond); // 吃完了唤醒厨师做饭
pthread_mutex_unlock(&mutex); // 解锁
}
}
void* cook(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex); // 加锁
if(counter == 1){
pthread_cond_wait(&cond, &mutex); // 有饭则 解锁并阻塞,等待唤醒,唤醒后加锁
}
printf("你的饭好了\n");
counter = 1;
pthread_cond_signal(&cond); // 饭做好了唤醒顾客吃饭
pthread_mutex_unlock(&mutex); // 解锁
}
}
int main()
{
pthread_mutex_init(&mutex, NULL); // 初始化定义mutex 和 cond
pthread_cond_init(&cond, NULL);
pthread_t cook_tid; // 初始化定义线程ID(顾客和厨师)
pthread_t cus_tid;
int ret;
ret = pthread_create(&cook_tid, NULL, cook, NULL); // 分别创建对应线程
if(ret != 0){
perror("create error");
return -1;
}
ret = pthread_create(&cus_tid, NULL, customer, NULL);
if(ret != 0){
perror("create error");
return -1;
}
pthread_join(cook_tid, NULL); // 执行线程等待
pthread_join(cus_tid, NULL);
pthread_mutex_destroy(&mutex); // 执行mutex与cond的销毁
pthread_cond_destroy(&cond);
}
实现四个顾客四个厨师
在同步实现的代码中,如果存在多种角色,就应该定义多个条件变量,各自处于各自的pcb等待队列中。
#include<stdio.h>
#include<pthread.h>
int counter = 0;
pthread_mutex_t mutex;
pthread_cond_t cond_cus; // 使用不同的条件变量,防止死锁
pthread_cond_t cond_cook;
void* customer(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
while(counter <= 0){
pthread_cond_wait(&cond_cus, &mutex);
}
printf("真好吃,再来一碗: %d\n",counter);
counter--;
pthread_cond_signal(&cond_cook);
pthread_mutex_unlock(&mutex);
}
}
void* cook(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
while(counter > 0){
pthread_cond_wait(&cond_cook, &mutex);
}
printf("你的饭好了:%d\n", counter);
counter++;
pthread_cond_signal(&cond_cus);
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond_cus, NULL);
pthread_cond_init(&cond_cook, NULL);
pthread_t cook_tid[4]; // 定义四个顾客、四个厨师
pthread_t cus_tid[4];
int ret;
for(int i = 0; i < 4; i++) // 创建这八个线程
{
ret = pthread_create(&cook_tid[i], NULL, cook, NULL);
if(ret != 0){
perror("create error");
return -1;
}
ret = pthread_create(&cus_tid[i], NULL, customer, NULL);
if(ret != 0){
perror("create error");
return -1;
}
}
pthread_join(cook_tid[0], NULL);
pthread_join(cus_tid[0], NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond_cook);
pthread_cond_destroy(&cond_cus);
}
生产者与消费者模型
生产者与消费者模型http://t.csdn.cn/GvZlZ