当前位置: 首页 > news >正文

Linux_线程的同步与互斥

目录

1、互斥相关概念 

2、代码体现互斥重要性

3、互斥锁 

3.1 初始化锁 

3.2 申请、释放锁 

3.3 加锁的思想

3.4 实现加锁 

3.5 锁的原子性 

4、线程安全 

4.1 可重入函数 

4.2 死锁 

5、线程同步 

5.1 条件变量初始化 

5.2 条件变量等待队列

5.3 唤醒等待队列

5.4 实现线程同步

结语 


前言:

        在Linux下,线程是一个很重要的概念,他可以提高多执行流的并发度,而同步与互斥是对线程的一种约束行为,比如当多个线程都访问同一个资源时,若不对该资源加以保护则会导致意料之外的错误。具体的保护措施是让线程访问共享资源时具有互斥性,即当一个线程访问时别的线程无法访问,通常用互斥锁来实现。而同步是为了让多个线程具有一定的顺序来访问共享内存,保障每个线程访问资源的机会是一样的。

1、互斥相关概念 

        线程之所以需要互斥,是因为多线程在访问共享资源时,可能该资源只允许被修改一次,但是其他线程在修改的时候“刹不住车”,导致该资源被修改多次,原因就是多个线程同时访问了该资源,如下图所示:

        在概念层面上,通常把共享资源叫做临界资源。在代码层面,把访问共享资源的代码叫做临界区


        当线程有了互斥约束后,就不会出现上述a=0时继续访问a的情况,如下图:

2、代码体现互斥重要性

        在实际生活中,某些有限的物品是不能出现负数的情况的,比如抢票,票为0时是不能继续抢票的,但是当实现多线程抢票时,若没有互斥的约束,则很容易发生票为0时还在抢票,模拟抢票的代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>using namespace std;int tickets = 1000; // 用多线程模拟抢票class threadData
{
public:threadData(int number){threadname = "线程-" + to_string(number);}public:string threadname;
};void *getTicket(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();while (true){if(tickets > 0){usleep(1000);printf("%s, 抢到一张票: %d\n", name, tickets); tickets--;}elsebreak;}printf("%s 退出\n", name);return nullptr;
}int main()
{vector<pthread_t> tids;vector<threadData *> thread_datas;for (int i = 1; i <= 4; i++){pthread_t tid;threadData *td = new threadData(i);thread_datas.push_back(td);pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);tids.push_back(tid);}//等待线程for (auto thread : tids){pthread_join(thread, nullptr);}//释放空间资源for (auto td : thread_datas){delete td;}return 0;
}

         运行结果:

        从结果可以看到,发生了负数票的情况,原因就是上面多线程代码没有任何互斥的约束。


        对上面代码进行分析找出其临界区,全局变量ticket是临界资源,因此代码中对ticket的访问就是临界区,如下图所示:

        为了解决上面的问题,只能使用互斥约束多线程,而互斥就必须用到互斥锁。 

3、互斥锁 

         实现互斥锁的步骤:

        1、创建一个锁变量。

        2、使用接口初始化该变量。

        3、在临界区处申请该锁。

        4、临界区代码执行完后释放锁。

        5、销毁锁。

        值得注意的是:只能用一把锁限制对临界区的访问,即线程要想访问临界区,则必须申请到该锁才能访问,没有申请到锁的线程就无法访问临界区。 


        申请锁的示意图如下:

3.1 初始化锁 

        初始化锁用到的接口介绍如下:

#include <pthread.h>
//销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);//初始化锁,方式1
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
//restrict mutex表示要初始化的锁
//restrict attr表示初始化的属性//初始化锁,方式2
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//定义在全局,则mutex锁就已经被初始化了

        pthead_mutex_t是库提供的数据类型,用于定义一个锁。方式2是一个全局变量初始化锁,若用方式2初始化一个锁则无需对该锁进行destroy。注意:若用方式2进行锁的初始化则该锁必须是全局的。

3.2 申请、释放锁 

        锁的初始化工作完成后,接下来就是申请锁,申请锁的接口介绍如下:

#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);//申请mutex锁int pthread_mutex_trylock(pthread_mutex_t *mutex);//申请mutex锁int pthread_mutex_unlock(pthread_mutex_t *mutex);//释放mutex锁

        pthread_mutex_lock申请不到锁会阻塞在该函数处,而pthread_mutex_trylock申请不到锁不会阻塞,会继续执行下面代码。

3.3 加锁的思想

        申请锁就是加锁,加锁的本质是用时间换来线程安全,让线程访问临界资源时串开访问,对临界区进行加锁时尽量缩小临界区的代码量,因为临界区的代码越少,执行的速度越快,则进程被cpu挂起的概念就越低,被cpu挂起的概念低了则可以减少其他线程等的时间,因为当申请到锁的线程被挂起了,那么其他的线程就算被cpu调度了也不能执行临界区的代码(因为其他线程没有持有锁),只能干等。

3.4 实现加锁 

        对上述代码实现加锁,代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>using namespace std;int tickets = 1000; // 用多线程模拟抢票class threadData
{
public:threadData(int number , pthread_mutex_t *mutex){threadname = "线程-" + to_string(number);lock = mutex;}public:string threadname;pthread_mutex_t *lock;
};void *getTicket(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();while (true){pthread_mutex_lock(td->lock); //申请锁if(tickets > 0){usleep(1000);printf("%s, 抢到一张票: %d\n", name, tickets);tickets--;pthread_mutex_unlock(td->lock);//释放锁}else{pthread_mutex_unlock(td->lock);//释放锁break;}//usleep(12); //先把此处的usleep屏蔽,观察抢票现象}printf("%s 退出\n", name);return nullptr;
}int main()
{pthread_mutex_t lock;//定义一个锁pthread_mutex_init(&lock, nullptr);//对该锁进行初始化vector<pthread_t> tids;vector<threadData *> thread_datas;for (int i = 1; i <= 3; i++){pthread_t tid;threadData *td = new threadData(i ,&lock);thread_datas.push_back(td);//要将锁也传给线程pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);tids.push_back(tid);}//等待线程for (auto thread : tids){pthread_join(thread, nullptr);}//释放空间资源for (auto td : thread_datas){delete td;}pthread_mutex_destroy(&lock);return 0;
}

         运行结果:

        从结果看,虽然没有出现负票的情况,但是发现只有一个线程在抢票,原因很简单,肯定是只有该进程申请到锁了,其他线程没申请到,那么为什么只有该线程能申请到,而其他线程申请不到呢?是因为这个线程刚释放完锁后他就立马再进行申请锁的动作了,他之所以可以比其他线程更快申请到锁的原因是“他离锁最近”,具体示意图如下:


        所以在一个线程释放锁后,可以手动对该线程进行sleep,让其他线程有机会去申请到锁,因此把上述代码中释放锁后面的usleep放开,就可以让其他线程申请到锁了,运行结果如下:

3.5 锁的原子性 

        从上文可以得知,当多线程访问共享资源时,若没有互斥约束,则会发生错误,所以对线程进行加锁的操作,但是锁本身也是共享资源,因为多线程都能看到锁并且申请他,那么申请锁的时候不好导致同样的问题吗?

        答案是不会,多线程访问共享资源之所以会发生意料之外的错误,是因为多线程对共享资源做修改操作的时候,这些修改操作在底层被转换成汇编语句,虽然上层看到的修改操作只有一句代码,但是在底层转换成两三句汇编指令,而cpu一次只能运算一句汇编指令,这就导致同一个操作没有真正被cpu执行完就被切换走了,等到下次继续执行该操作时,从内存中读取的数据可能已经被别的线程修改了,这就导致了意料之外的错误。而申请锁的动作只有一句汇编指令,他的状态只有两种:1、要么没申请到锁,2、要么申请到锁。不存在执行一半被切走的可能,通常把这种状态叫做原子性,因此锁是具有原子性的。

4、线程安全 

        线程安全指的是在多线程的并行下,访问某些资源时,不会导致该资源的数据损坏或出现意料之外的错误,线程与线程之间不会互相干扰对方的操作,多线程能够安全的执行下去,把这叫做线程安全。

4.1 可重入函数 

        可重入函数值得是当同一个函数被多个线程调用时,调用的结果不会产生任何的问题,比如不会导致数据损坏或者资源泄漏,则该函数被称为可重入函数,否则,是不可重入函数。

4.2 死锁 

         死锁指的是当线程申请锁时造成了循环申请,也就是说线程1要申请线程2的锁,而线程2要申请线程1的锁,造成死循环称之为死锁,具体示意图如下:

        造成死锁的四个必要条件:

1、互斥条件:一把锁只能被一个线程申请。
2、请求与保持条件:多线程之间互相申请对方的锁,但是对方就是不释放该锁。
3、不剥夺条件 :不释放对方的锁,即使要申请的锁在对方手里也不主动释放。
4、循环等待条件 : 多线程循环等待彼此的资源。

        只要不满足上面4个条件是任何一个,则就造成不了死锁。 

5、线程同步 

        线程同步的目的是让每个线程申请锁的能力是有顺序性的,即每个线程都可以公平的申请到锁,通常是定义一个条件变量,然后将线程放入等待队列中(申请的前提是该线程必须持有锁),申请到锁的线程就能够进入等待队列中等待了,进入等待队列时线程会自动释放锁,目的是让下一个线程申请锁然后也入队,因此条件变量必须搭配锁才能使用。

        将线程放入等待队列的示意图如下:


        唤醒等待队列里的线程去申请锁:

        等待队列申请锁的逻辑:首先需要唤醒该等待队列,然后队列里的第一个线程可以重新去申请锁,访问临界资源结束后,释放锁的线程会回到队列的末尾,如此逻辑就能够实现线程同步了

5.1 条件变量初始化 

        条件变量的初始化逻辑和锁的初始化逻辑相似,都是有两种初始化方式,具体接口如下:

#include <pthread.h>
//销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);//条件变量初始化方式1
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
//restrict cond表示要初始化的条件变量的地址
//attr表示条件变量初始化的属性设置//条件变量初始化方式2
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//在全局定义完成初始化

5.2 条件变量等待队列

        在条件变量完成初始化后,需要将线程放入条件变量的等待队列中, 这个过程只需要调用函数pthread_cond_wait即可完成,但是要注意调用该函数时当前线程必须是持有锁的,所以使用条件变量必须依赖锁,pthread_cond_wait函数介绍如下:

#include <pthread.h>int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
//cond表示将该线程放入哪个条件变量的队列
//mutex表示等待队列被唤醒后可申请的锁

        当线程调用此函数时,会释放已经申请的锁然后在等待队列中排队,所以线程的执行流会阻塞在该函数处。

5.3 唤醒等待队列

        当调用,唤醒函数介绍如下:

#include <pthread.h>int pthread_cond_broadcast(pthread_cond_t *cond);
//cond表示要唤醒的条件变量等待队列,该函数被调用一次则整个队列都被唤醒int pthread_cond_signal(pthread_cond_t *cond);
//cond表示要唤醒的条件变量等待队列,该函数被调用一次则只唤醒队头的线程

5.4 实现线程同步

         上文中抢票代码的逻辑是线程释放锁后对该线程进行sleep,这么做让其他线程有了申请锁的机会,其实这也是同步的一种方法,只不过sleep的时间不好控制,而现在我们无需对线程进行sleep也可以实现同步,即使用条件变量进行同步,让系统去维护同步机制,可以更好的控制同步。

        实现线程同步的代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>using namespace std;int tickets = 1000; // 用多线程模拟抢票
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//全局初始化class threadData
{
public:threadData(int number , pthread_mutex_t *mutex){threadname = "线程-" + to_string(number);lock = mutex;}public:string threadname;pthread_mutex_t *lock;
};void *getTicket(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();while (true){pthread_mutex_lock(td->lock); //申请锁pthread_cond_wait(&cond,td->lock);//将线程放入等待队列if(tickets > 0){//usleep(1000);printf("%s, 抢到一张票: %d\n", name, tickets);tickets--;pthread_mutex_unlock(td->lock);//释放锁}else{pthread_mutex_unlock(td->lock);//释放锁break;}}printf("%s 退出\n", name);return nullptr;
}int main()
{pthread_mutex_t lock;//定义一个锁pthread_mutex_init(&lock, nullptr);//对该锁进行初始化vector<pthread_t> tids;vector<threadData *> thread_datas;for (int i = 1; i <= 3; i++)//创建3个线程{pthread_t tid;threadData *td = new threadData(i ,&lock);thread_datas.push_back(td);//要将锁也传给线程pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);tids.push_back(tid);}sleep(2);//目的是让线程全部都放入队列中,然后再进行唤醒//唤醒队列while(true){pthread_cond_signal(&cond);}//等待线程for (auto thread : tids){pthread_join(thread, nullptr);}//释放空间资源for (auto td : thread_datas){delete td;}pthread_mutex_destroy(&lock);return 0;
}

        运行结果:

        从结果可以看到,没有出现负票的情况,并且所有线程都在抢票。这里注意pthread_mutex_lock和pthread_cond_wait两个函数对锁的申请和释放逻辑,调用pthread_mutex_lock时线程会申请锁,然后调用pthread_cond_wait时,线程会释放锁,并且阻塞在该函数处等待被唤醒,被唤醒后该线程又重新申请锁,申请成功后执行临界区代码。

结语 

        以上就是关于线程的同步与互斥讲解,若使用多线程进行并发式的执行程序,那么同步和互斥是必不可少的保护措施,他保障了多线程并发执行时线程的安全,防止出现意料之外的错误,因此对临界资源进行同步和互斥是多线程执行时非常重要的一步。

        最后如果本文有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Elasticsearch基础(五):使用Kibana Discover探索数据
  • Mybatis——动态SQL常用标签
  • JavaWeb笔记_Cookie
  • 企业微信PC版应用跳转到默认浏览器,避坑指南,欢迎补充(Vue项目版)。。。
  • IVI(In-Vehicle Infotainment,智能座舱的信息娱乐系统)
  • 深度学习落地实战:人脸面部表情识别
  • 【Android Framewrok】Handler源码解析
  • L298N的输出电流与电压
  • 我在百科荣创企业实践——简易函数信号发生器(5)
  • CentOS Mysql8 数据库安装
  • 【NLP】关于参数do_sample的解释
  • php编译安装
  • 浅聊 Three.js 屏幕空间反射SSR-SSRShader
  • 【JavaScript 算法】滑动窗口:处理子数组问题
  • 打造直播工具详解:从零开始开发直播美颜SDK
  • 时间复杂度分析经典问题——最大子序列和
  • [NodeJS] 关于Buffer
  • Android开发 - 掌握ConstraintLayout(四)创建基本约束
  • JavaSE小实践1:Java爬取斗图网站的所有表情包
  • Mysql优化
  • Python - 闭包Closure
  • SSH 免密登录
  • Vue组件定义
  • Web标准制定过程
  • 闭包,sync使用细节
  • 程序员最讨厌的9句话,你可有补充?
  • 发布国内首个无服务器容器服务,运维效率从未如此高效
  • 好的网址,关于.net 4.0 ,vs 2010
  • 名企6年Java程序员的工作总结,写给在迷茫中的你!
  • 如何设计一个比特币钱包服务
  • 如何正确配置 Ubuntu 14.04 服务器?
  • 通过来模仿稀土掘金个人页面的布局来学习使用CoordinatorLayout
  • 原创:新手布局福音!微信小程序使用flex的一些基础样式属性(一)
  • Hibernate主键生成策略及选择
  • kubernetes资源对象--ingress
  • 机器人开始自主学习,是人类福祉,还是定时炸弹? ...
  • 新海诚画集[秒速5センチメートル:樱花抄·春]
  • ​1:1公有云能力整体输出,腾讯云“七剑”下云端
  • #QT项目实战(天气预报)
  • (2022版)一套教程搞定k8s安装到实战 | RBAC
  • (function(){})()的分步解析
  • (笔记)Kotlin——Android封装ViewBinding之二 优化
  • (原創) 如何使用ISO C++讀寫BMP圖檔? (C/C++) (Image Processing)
  • ******之网络***——物理***
  • .NET 8.0 发布到 IIS
  • .NET Conf 2023 回顾 – 庆祝社区、创新和 .NET 8 的发布
  • .net core 调用c dll_用C++生成一个简单的DLL文件VS2008
  • .Net MVC + EF搭建学生管理系统
  • .Net Remoting(分离服务程序实现) - Part.3
  • .net 调用海康SDK以及常见的坑解释
  • .NET8 动态添加定时任务(CRON Expression, Whatever)
  • @ComponentScan比较
  • @FeignClient 调用另一个服务的test环境,实际上却调用了另一个环境testone的接口,这其中牵扯到k8s容器外容器内的问题,注册到eureka上的是容器外的旧版本...
  • []指针
  • [15] 使用Opencv_CUDA 模块实现基本计算机视觉程序