线程安全:多个执行流对临界资源的争抢访问,但是并不会造成数据的二义性

线程的安全主要是通过同步和互斥来实现的
同步:通过条件判断保证对临界资源访问的合理性
互斥:通过同一时间的唯一访问实现对临界资源访问的安全性

如何实现同步与互斥

互斥的实现

互斥量

互斥量本身只有0/1的计数器,描述了一个临界资源当前的访问状态,所有执行流在访问临界资源前都需要判断临界资源状态是否允许访问,如果是允许访问状态就可以让执行流访问临界资源,并修改临界资源的状态为不可访问状态,这期间如果有其他执行流想要访问,则会让执行流进行等待。等到访问临界资源的执行流离开后,资源被释放后,其他执行流才可以进行争抢访问

具体操作流程以及接口介绍

  1. 定义互斥量变量(pthread_mutex_t mutex)
  2. 初始化互斥量变量,接口pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr)
    定义并初始化pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  3. 在访问临界资源之前进行加锁操作:不能加锁则表示已有线程访问,需要等待;可以加锁就修改临界资源访问状态,然后去访问临界资源。属于阻塞加锁,接口pthread_mutex_lock(pthread_mutex_t *mutex) ;还有一种是能加锁就加锁并访问资源,不能加锁就立即报错返回。属于非阻塞加锁,接口pthread_mutex_trylock(pthread_mutex_t *mutex)
  4. 在临界资源访问完毕之后进行解锁操作:将资源状态置为可访问状态,将其他执行流唤醒。接口pthread_mutex_unlock(pthread_mutex_t *mutex)
  5. 销毁互斥锁,接口pthread_mutex_destroy(pthread_mutex_t *mutex)

我们先来看看不进行互斥,去进行临界资源访问的例子----黄牛抢票

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>//票数
int ticket = 100;//线程函数
void *thr_scalpers(void *arg)
{while (1){if (ticket > 0){usleep(1000);printf("i got a ticket:%d\n", ticket);--ticket;}else{pthread_exit(NULL);}}return NULL;
}int main()
{//4个线程pthread_t tid[4];int i, ret;for (i = 0; i < 4; ++i){//创建4个线程并执行ret = pthread_create(&tid[i], NULL, thr_scalpers, NULL);if (ret != 0){printf("thread create error");return -1;}}for (i = 0; i < 4; ++i){pthread_join(tid[i], NULL);}return 0;
}

执行结果

先不看前面抢票的结果,就看最后3次抢票,都抢到了负数的票,这相当于抢到了假票,是不安全的。原因是当只是一张票时,有一个线程运行到if语句并进入时,休眠了1毫秒,其他的线程也乘机而入,也进入到if语句中,最后到抢到了假的票,也就是负数。

我们再来看看假如互斥量操作后会是怎样的结构

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>//票数
int ticket = 100;
//1、定义e互斥变量
pthread_mutex_t mutex;//线程函数
void *thr_scalpers(void *arg)
{while (1){//3、加锁一定是只加锁临界资源的访问pthread_mutex_lock(&mutex);if (ticket > 0){usleep(1000);printf("i got a ticket:%d\n", ticket);--ticket;//4、解锁pthread_mutex_unlock(&mutex);}else{//加锁后在任意有可能退出线程的地方都要解锁pthread_mutex_unlock(&mutex);pthread_exit(NULL);}}return NULL;
}int main()
{//4个线程pthread_t tid[4];int i, ret;//2、互斥变量的初始化一定要放在线程创建之前pthread_mutex_init(&mutex, NULL);for (i = 0; i < 4; ++i){//创建4个线程并执行ret = pthread_create(&tid[i], NULL, thr_scalpers, NULL);if (ret != 0){printf("thread create error");return -1;}}for (i = 0; i < 4; ++i){pthread_join(tid[i], NULL);}//5、一定要在所以线程不使用互斥变量后在销毁pthread_mutex_destroy(&mutex);return 0;
}

运行结果

我们发现可以安全得进行抢票,并不会出现抢到负数的票了

我们发现所有的线程都是通过同一个互斥量来实现互斥的,也就意味着这个互斥变量也是一个临界资源,大家都可以访问得到,所以互斥变量本身都不是安全的,那这样子如何保证别的线程的安全呢?其实设计互斥变量时就将它自身的计数器置为原子性操作

如果mutex是一个普通变量的情况,操作系统首先会将mutex的值加载到寄存器中,再判断寄存器中的值是否为1,如果为1就加锁,并将0写入到mutex中,然后访问临界资源,如果是0则让线程等到。但是在这个加锁并将0写到mutex中这个操作并非原子操作,如果时间片到了,mutex的值还是1,这时候会有其他线程进来加锁访问,会导致线程的不安全。
而情况并非这样,操作系统知道你是一个互斥变量,会先将寄存器的值置为0,然后直接将寄存器的值与内存中的mutex数据进行交换,这个交换操作是原子性操作(单条指令),这时候mutex的值肯定是0了,当有线程访问时肯定会是等待的了,然后再根据寄存器中的值进行判断是否可以加锁。加锁后再解锁,将mutex置回来

同步的实现

我们再来看看黄牛抢票的代码,如果我们打印线程的tid,也就是看哪个线程抢到的票

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>//票数
int ticket = 100;
//1、定义e互斥变量
pthread_mutex_t mutex;//线程函数
void *thr_scalpers(void *arg)
{while (1){//3、加锁一定是只加锁临界资源的访问pthread_mutex_lock(&mutex);if (ticket > 0){usleep(1000);printf("%p-i got a ticket:%d\n",pthread_self(), ticket);--ticket;//4、解锁pthread_mutex_unlock(&mutex);}else{//加锁后在任意有可能退出线程的地方都要解锁pthread_mutex_unlock(&mutex);pthread_exit(NULL);}}return NULL;
}int main()
{//4个线程pthread_t tid[4];int i, ret;//2、互斥变量的初始化一定要放在线程创建之前pthread_mutex_init(&mutex, NULL);for (i = 0; i < 4; ++i){//创建4个线程并执行ret = pthread_create(&tid[i], NULL, thr_scalpers, NULL);if (ret != 0){printf("thread create error");return -1;}}for (i = 0; i < 4; ++i){pthread_join(tid[i], NULL);}//5、一定要在所以线程不使用互斥变量后在销毁pthread_mutex_destroy(&mutex);return 0;
}


我们发现都是一个线程在执行,也就是说互斥并不保证资源分配的合理性,只保证了资源分配的安全性

条件变量

条件变量实现同步原理:是去判断当前线程是否满足获取资源的条件,当线程获取条件不满足的时候,调用阻塞接口,使线程阻塞,将pcb挂到等待队列上。等到条件满足的时候通过唤醒接口唤醒等待队列上的阻塞了的线程

条件变量提供了一个pcb等待队列,以及使线程阻塞的接口和唤醒线程的接口

条件的判断是用户进行的操作,判断线程是否满足条件,不满足的时候调用条件变量接口使线程等待

操作流程

  1. 定义条件变量 pthread_cond_t cond
  2. 初始化条件变量 pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr)
    定义并初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER
  3. 获取资源条件,不满足时挂起休眠pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)条件变量时搭配互斥变量一起使用的(原因是条件变量本事也是一个临界资源,需要被保护)
    int pthread_cond_timedwait(pthread_cond_t *cond, 一直等待添加满足后唤醒pthread_mutex_t *mutex, const struct timespec *abstime)这个接口是可以设置阻塞超时后可以自行唤醒
  4. 唤醒线程 pthread_cond_signal(pthread_cond_t *cond)唤醒至少一个线程; pthread_cond_broadcast(pthread_cond_t *cond)唤醒所有等待的线程
  5. 销毁条件变量 pthread_cond_destroy(pthread_cond_t *cond)

我们拿顾客和厨师来模拟实现同步的过程(保证资源利用合理性)
顾客(线程A) 厨师(线程B) 碗(资源)
顾客操作:
1、预定这个碗(加锁)
2、看碗是否有饭,没有饭就等着,把碗给厨师(访问资源前先判断是否满足访问条件,解锁,不满足就阻塞等待)
3、碗有饭,吃饭(加锁,满足访问条件,利用并占有该资源)
4、吃完后,奖励自己再来一碗(释放资源,唤醒另一个线程)
5、把碗还给厨师(解锁)

厨师操作:
1、把饭放到指定碗中(加锁)
2、看碗是否有饭,有饭就等着,把碗给顾客(访问资源前先判断是否满足访问条件,解锁,不满足就阻塞等待)
3、碗没有饭,做饭(加锁,满足访问条件,利用并占有该资源)
4、做完饭后叫顾客吃饭(释放资源,唤醒另一个线程)
5、把碗给客户(解锁)

代码实现

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>//默认0表示碗中没有饭
int bowl = 0;//实现线程间对bowl变量访问的同步操作
pthread_cond_t cond;
//保护bowl变量的访问操作
pthread_mutex_t mutex;void *thr_cook(void *arg)
{while (1){//加锁pthread_mutex_lock(&mutex);if (bowl != 0)//表示有饭,不满足做饭条件{//解锁,并让厨师线程等待,被唤醒后再加锁//在该接口中解锁和休眠操作是一步完成的,保证操作的原子性pthread_cond_wait(&cond, &mutex);//3步操作一个接口完成}bowl = 1;//能够走下来表示没饭,做完后置为1printf("i made a bowl of rice\n");//唤醒顾客吃饭pthread_cond_signal(&cond);//解锁pthread_mutex_unlock(&mutex);}return NULL;
}
void *thr_customer(void *arg)
{while (1){//加锁pthread_mutex_lock(&mutex);if (bowl != 1)//没有饭,不满足吃饭条件{//没有饭,先解锁,后等待pthread_cond_wait(&cond, &mutex);}bowl = 0;//能够走下来表示有饭,吃完后置为0printf("i had a bowl of rice, it was delicious\n");//唤醒厨师做饭pthread_cond_signal(&cond);//解锁pthread_mutex_unlock(&mutex);}return NULL;
}int main()
{//厨师线程pthread_t cook_tid;//顾客线程pthread_t customer_tid;int ret;pthread_mutex_init(&mutex, NULL);pthread_cond_init(&cond, NULL);ret = pthread_create(&cook_tid, NULL, thr_cook, NULL);if (ret != 0){printf("pthread_create error\n");return -1;}ret = pthread_create(&customer_tid, NULL, thr_customer, NULL);if (ret != 0){printf("pthread_create error\n");return -1;}//等待厨师和顾客线程pthread_join(cook_tid, NULL);pthread_join(customer_tid, NULL);        pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);return 0;
}

运行结果:我们发现通过同步与互斥,就可以实现对临界资源访问的安全性和合理性,不会出现没饭也吃的情况

但是如果存在多个顾客和多个厨师,会发生什么情况呢?
只提供修改主函数中的代码,其他代码和上面一样

int main()
{//厨师线程pthread_t cook_tid[4];//顾客线程pthread_t customer_tid[4];int ret, i;pthread_mutex_init(&mutex, NULL);pthread_cond_init(&cond, NULL);for (i = 0; i < 4; ++i){ret = pthread_create(&cook_tid[i], NULL, thr_cook, NULL);if (ret != 0){printf("pthread_create error\n");return -1;}}for (i = 0; i < 4; ++i){ret = pthread_create(&customer_tid[i], NULL, thr_customer, NULL);if (ret != 0){printf("pthread_create error\n");return -1;}}pthread_join(cook_tid[0], NULL);pthread_join(customer_tid[0], NULL);pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);return 0;
}

运行结果:

我们发现,当存在多个顾客和多个厨师时,出现了没饭也吃,有饭也做的情况,这肯定不是我们想要的,那原因处在哪呢?

一开始有多个顾客线程,因为没有饭都进行等待,当一个厨师做好饭后,因为调用的是pthread_cond_signal接口唤醒顾客,唤醒了至少1个顾客,也就是多个顾客,当有一个顾客加锁成功后,就去吃饭,剩下的顾客线程只能卡在加锁这里。加锁成功吃完饭的顾客线程去唤醒厨师并进行解锁,由于cpu是时间片调用线程的,加锁的不一定时厨师,有可能是卡在加锁那块的顾客线程,当顾客线程加锁后就去吃饭,但是此刻是没有饭的。所以就发生了错误如何避免这个错误呢?在第二步的条件判断应该改为循环判断,这样即使顾客加锁成功被唤醒后发现没饭也不会去吃了,而是看大没饭就继续休眠,有饭再吃

只提供修改两个线程入口函数的代码,其他代码和上面一样

void *thr_cook(void *arg)
{while (1){//加锁pthread_mutex_lock(&mutex);while (bowl != 0)//表示有饭,不满足做饭条件{//解锁,并让厨师线程等待,被唤醒后再加锁//在该接口中解锁和休眠操作是一步完成的,保证操作的原子性pthread_cond_wait(&cond, &mutex);//3步操作一个接口完成}bowl = 1;//能够走下来表示没饭,做完后置为1printf("i made a bowl of rice\n");//唤醒顾客吃饭pthread_cond_signal(&cond);//解锁pthread_mutex_unlock(&mutex);}return NULL;
}
void *thr_customer(void *arg)
{while (1){//加锁pthread_mutex_lock(&mutex);while (bowl != 1)//没有饭,不满足吃饭条件{//没有饭,先解锁,后等待pthread_cond_wait(&cond, &mutex);}bowl = 0;//能够走下来表示有饭,吃完后置为0printf("i had a bowl of rice, it was delicious\n");//唤醒厨师做饭pthread_cond_signal(&cond);//解锁pthread_mutex_unlock(&mutex);}return NULL;
}

运行结果:

虽然不会出现没饭吃饭,有饭做饭的情况,但是我们发现程序发生了阻塞,不继续往下执行了,这又是什么原因呢?
条件变量只有一个,意味着等待队列只有一个,顾客没饭吃就挂到等待队列上,厨师不能做饭也要挂到等待队列上,假设一个厨师线程做完饭后,要去唤醒一个顾客线程去吃饭,但是却唤醒的是厨师线程(顾客线程和厨师线程都在同一个队列),唤醒的厨师线程发现有饭,又重新挂载等待队列中,从而导致程序阻塞如何避免这种情况呢?不同角色的线程应该在不同的等待队列上进行等待,这样等唤醒的时候,就不会唤醒同类型的线程了,因此存在多个角色线程,就应该设置多个条件变量(一个条件变量对应一个等待队列)

完整正确代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>//默认0表示碗中没有饭
int bowl = 0;//实现线程间对bowl变量访问的同步操作
pthread_cond_t cook_cond;
pthread_cond_t customer_cond;
//保护bowl变量的访问操作
pthread_mutex_t mutex;void *thr_cook(void *arg)
{while (1){//加锁pthread_mutex_lock(&mutex);while (bowl != 0)//表示有饭,不满足做饭条件{//解锁,并让厨师线程等待,被唤醒后再加锁//在该接口中解锁和休眠操作是一步完成的,保证操作的原子性pthread_cond_wait(&cook_cond, &mutex);//3步操作一个接口完成}bowl = 1;//能够走下来表示没饭,做完后置为1printf("i made a bowl of rice\n");//唤醒顾客吃饭pthread_cond_signal(&customer_cond);//解锁pthread_mutex_unlock(&mutex);}return NULL;
}
void *thr_customer(void *arg)
{while (1){//加锁pthread_mutex_lock(&mutex);while (bowl != 1)//没有饭,不满足吃饭条件{//没有饭,先解锁,后等待pthread_cond_wait(&customer_cond, &mutex);}bowl = 0;//能够走下来表示有饭,吃完后置为0printf("i had a bowl of rice, it was delicious\n");//唤醒厨师做饭pthread_cond_signal(&cook_cond);//解锁pthread_mutex_unlock(&mutex);}return NULL;
}int main()
{//厨师线程pthread_t cook_tid[4];//顾客线程pthread_t customer_tid[4];int ret, i;pthread_mutex_init(&mutex, NULL);pthread_cond_init(&cook_cond, NULL);pthread_cond_init(&customer_cond, NULL);for (i = 0; i < 4; ++i){ret = pthread_create(&cook_tid[i], NULL, thr_cook, NULL);if (ret != 0){printf("pthread_create error\n");return -1;}}for (i = 0; i < 4; ++i){ret = pthread_create(&customer_tid[i], NULL, thr_customer, NULL);if (ret != 0){printf("pthread_create error\n");return -1;}}pthread_join(cook_tid[0], NULL);pthread_join(customer_tid[0], NULL);pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cook_cond);pthread_cond_destroy(&customer_cond);return 0;
}

运行结果:

使用注意事项总结:
1、条件变量需要搭配互斥锁一起使用
2、在每一个有可能退出线程的地方都需要解锁
3、条件变量使用时对条件的判断应该使用while循环来判断
4、多种角色线程应该使用多个条件变量

练习:生产者与消费者模型

信号量

信号量可以用于实现线程或者进程同步与互斥(主要用于同步)
信号量 = 一个计数器 + pcb等待队列
同步原理:通过自身计数器对资源进行计数,并通过计数器的资源计数,判断进程/线程是否能够符合访问资源的条件,若符合就可以访问,若不符合则调用提供的接口让进程/线程加入到pcb等待队列中;其他进程/线程促使条件满足之后,可以唤醒pcb等待队列上的进程/线程

互斥原理:保证计数器的计数不大于1,就保证了资源只有一个,并且同一时间只能被一个进程/线程访问

操作流程
1、定义信号量 sem_t sem
2、初始化信号量int sem_init(sem_t *sem, int pshared, unsigned int value) 参数内容(sem:我们定义的信号量;pshared:标识该信号量用于进程还是线程,0表示用于线程间,非0表示用于进程间;value:初始化信号量,初识资源数量有多少该值就为多少) 返回值:成功返回0,失败返回-1
3、在访问临界资源之前,先访问信号量,判断是否能够访问,计数-1。接口1int sem_wait(sem_t *sem) 通过自身计数判断是否满足访问条件,不满足就一直阻塞;接口2int sem_trywait(sem_t *sem) 通过自身计数判断是否满足访问条件,不满足就报错返回;接口3int sem_timewait(sem_t *sem, const struct timespec *abs_timeout) 通过自身计数判断是否满足访问条件,当不满足就等待指定的时间,超时就报错返回
4、促使访问条件满足,计数+1,唤醒阻塞线程/进程int sem_post(sem_t *sem)
5、销毁信号量 int sem_destroy(sem_t *sem)

练习:通过信号量实现一个生产者与消费者模型

Linux 线程如何实现同步与互斥相关推荐

  1. linux线程同步互斥说法,linux线程间的同步与互斥知识点总结

    在线程并发执行的时候,我们需要保证临界资源的安全访问,防止线程争抢资源,造成数据二义性. 线程同步: 条件变量 为什么使用条件变量? 对临界资源的时序可控性,条件满足会通知其他等待操作临界资源的线程, ...

  2. 用于线程间的同步与互斥-信号量sem

    一.线程 首先我们说一下什么是线程.线程是计算机中独立运行的最小单位,在运行时占用很少的系统资源,由于每个线程占用的CPU时间是由系统分配的,因此我们可以把线程看作为系统分配CPU时间的基本单位.在我 ...

  3. linux 线程管理、同步机制等

    线程 学了那么多有关进程的东西,一个作业从一个进程开始,如果你需要执行其他的东西你可以添加一些进程,进程之间可以通信.同步.异步.似乎所有的事情都可以做了. 对的,进程是当初面向执行任务而开发出来的, ...

  4. 【Linux系统编程】同步和互斥的概念

    00. 目录 文章目录 00. 目录 01. 概述 02. 互斥的概念 03. 同步的概念 04. 总结 05. 附录 01. 概述 现代操作系统基本都是多任务操作系统,即同时有大量可调度实体在运行. ...

  5. linux之多任务的同步与互斥

    1.操作系统: 基本都是多任务操作系统,即同时有大量可调度实体在运行.在多任务操作系统中. 2.同时运行的多个任务可能产生的问题 1) 都需要访问/使用同一种资源           2) 多个任务之 ...

  6. 线程安全、同步与互斥机制以及死锁的产生与实现

    线程安全:多个执行流对临界资源争抢访问,但是不会出现数据二义性. 线程安全的实现:              同步:通过条件判断保证对临界资源访问的合理性.              互斥:通过同一时 ...

  7. C++多线程:Linux 线程通信,唤醒,互斥锁(未完待续)

    c++ multi thread message sending and notify 线程通信常用的方法有共享内存和消息传递,推荐使用消息传递. 最常用的就是管道了,可以使用匿名管道或者命名管道. ...

  8. 【Linux系统编程】线程同步与互斥:POSIX无名信号量

    信号量概述 信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问. 编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 ...

  9. 50.Linux 线程三 同步

    Linux系统中常用实现线程同步的方式有三种,分别为互斥锁.条件变量与信号量. 下面将对这三种方式逐一进行讲解. 4.1 互斥锁 使用互斥锁实现线程同步时,系统会为共享资源添加一个称为互斥锁的标记,防 ...

最新文章

  1. CCIE-MPLS基础篇-实验手册
  2. 程序员大神用 React “复刻”实现了一个 Windows 11
  3. java导出oracle到excel_java实现将oracle表中的数据导出到excel表里
  4. java 反射 速度_Java 反射获取类方法速率和实现方式
  5. sparkstreaming自定义kafka
  6. Java字符串中最长回文子字符串
  7. Ubuntu Linux 环境变量PATH设置
  8. SpringBoot下Mybatis-注解动态sql开发的坑
  9. Directx工具修复工具,专注修复C++动态链接DLL文件
  10. nodejs tinypng 压缩
  11. 2020-11-02-Ubuntu 20.04安装Anaconda3-卸载Anaconda3-笔记
  12. gma 教程 | 气候气象 | 基于 彭曼-蒙提斯法(Penman-Monteith)计算日作物参考蒸散量(ET0)
  13. Scratch软件编程等级考试一级——20210911
  14. python模拟勒索病毒
  15. c语言成绩与平均分问题,用C语言编程平均分数
  16. 如何用python爬取数据_入门用Python进行Web爬取数据:为数据科学项目提取数据的有效方法...
  17. 在线Base64编码 = 图片
  18. 使用 redis 连接指定端口的 redis 数据库
  19. HTML基础笔记笔记
  20. 上海社保查询 最强攻略

热门文章

  1. 箱线图怎么判断异常值_极简统计学---箱线图[2]
  2. 怎么在计算机修复flash,win10系统怎么用flash修复器?教你用flash修复器修复视频的方法...
  3. springcloud整合php,详细讲解springcloud的组件之RestTemplate集成的Ribbbon
  4. 什么是网络计算机有什么优点,ISDN是什么?它有什么优点?
  5. 使用oracle sql profile固定执行计划
  6. 安卓暗黑模式软件_安卓微信暗黑模式(深色模式)怎么开启?手机什么条件才支持?...
  7. Jsp servlet mysql 学生信息管理系统
  8. Eclipse集成Maven功能
  9. 基于JAVA+Servlet+JSP+MYSQL的在线购物系统
  10. 基于JAVA+SpringBoot+Mybatis+MYSQL的停车场管理系统