Linux学习笔记: https://blog.csdn.net/2301_80220607/category_12805278.html?spm=1001.2014.3001.5482 前言: 在上篇我们已经学习了关于线程的大部分知识,包括线程概念和线程控制等内容,今天我们来学习一下使用线程要做到的很重要的一步,那就是要保证线程的同步与互斥,从而确保线程安全问题,我们将通过一些生活中的例子和代码示例来理解如何做到线程的互斥与同步 目录 前提知识 线程的互斥 什么是线程的互斥 为什么需要线程的互斥 互斥的实现方法:互斥量 1. 互斥量的概念 2. 互斥量的接口函数 初始化与销毁 加锁与解锁 尝试加锁 3. 互斥量的使用 互斥锁实现互斥的原理 互斥量的使用场景 保护共享资源 避免竞态条件 互斥量的注意事项 死锁问题 性能开销 总结 前提知识在深入探讨线程的互斥与同步之前,我们需要了解一些基本概念:
线程的互斥什么是线程的互斥互斥是指在同一时刻,只允许一个线程访问共享资源。互斥机制确保了一个线程在访问共享资源时,其他线程不能同时访问该资源,从而避免了竞态条件的发生。 为什么需要线程的互斥在多线程编程中,多个线程可能会同时访问共享资源。如果没有互斥机制,可能会导致以下问题:
下面我们通过一个春节购票系统来看一下会有什么问题: 我们定义一个全局变量count=1000表示有1000张票,我们再创建四个新线程,这四个新线程执行抢票工作,主线程等待回收它们,抢票的逻辑具体分为以下三步: 1、先检查是否还有余票。if(count>0) 2、如果有余票就让count--,同时子线程返回抢票的那一步 3、如果没有余票就退出 [code]#include<iostream> #include<pthread.h> #include<unistd.h> using namespace std; int count=1000; void *GetTickets(void *args) { int thread_id = (int)(intptr_t)args; // 获取线程ID while(1) { if(count>0) { usleep(10000); //模拟购票准备工作所需时间 printf("[pthread %d] get a picket, the picket number: %d\n",thread_id,count); --count; } else { //如果没票,直接退出 break; } } return nullptr; } int main() { //创建四个新线程 pthread_t tids[4]; for(int i=0;i<4;i++) { pthread_create(&tids[i],nullptr,GetTickets,(void*)(intptr_t)(i+1)); } //阻塞等待回收线程 for(int i=0;i<4;i++) { pthread_join(tids[i],nullptr); } return 0; }[/code]执行结果(只截取了最后几行): ![]() 我们发现出现了错误,按照逻辑票应该是一张一张减少的,而且最终票数到0之后就不能再购买了,但是这里多购买了三张,那么原因是什么呢? 该错误是由以下两个原因组成的:
简单点来说就是在一个线程执行购票过程的时候,其它线程也可能会进入进行购票,因为线程是并发执行的,最终就可能会导致票数已经为0了,但是有些进程已经在票数为0前进入到if判断中了,就可能导致以上问题 ![]() 根本原因就是--操作并不是原子的(简单来说并不是一次完成的),而是对应三条汇编指令:
要做到以上三点,本质上就是需要一把锁,在一个线程进入临界区时将临界区的入口锁住不让其它线程进入,Linux提供的这个锁叫做互斥量 互斥的实现方法:互斥量1. 互斥量的概念互斥量是一种用于多线程编程的同步机制,用于确保同一时刻只有一个线程可以访问共享资源。互斥量的核心思想是通过加锁(Lock)和解锁(Unlock)操作来控制对共享资源的访问,从而避免多个线程同时修改共享资源导致的数据不一致或竞态条件。 互斥量通常用于保护临界区,即访问共享资源的代码段。当一个线程进入临界区时,它会先加锁;当线程离开临界区时,它会解锁,允许其他线程进入。 ![]() 2. 互斥量的接口函数![]() 1、互斥量的接口函数头文件都是 #include<pthread.h> 2、返回值都是:成功返回0,失败返回相应的错误码 初始化与销毁[code]pthread_mutex_init[/code]用于初始化互斥量。 [code]int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);[/code]
用于销毁互斥量,释放相关资源。 [code]int pthread_mutex_destroy(pthread_mutex_t *mutex);[/code]
加锁与解锁[code]pthread_mutex_lock[/code]用于加锁互斥量。如果互斥量已被其他线程锁定,当前线程会被阻塞,直到互斥量被解锁。 [code]int pthread_mutex_lock(pthread_mutex_t *mutex);[/code]
用于解锁互斥量,允许其他线程加锁。 [code]int pthread_mutex_unlock(pthread_mutex_t *mutex);[/code]
尝试加锁[code]pthread_mutex_trylock[/code]尝试加锁互斥量。如果互斥量已被其他线程锁定,函数立即返回,不会阻塞当前线程。 [code]int pthread_mutex_trylock(pthread_mutex_t *mutex);[/code]参数:
在锁的初始化上除了上面用函数来初始化外,我们也可以直接初始化: ![]() 这种初始化的方法叫做静态初始化,它将锁的声明、定义和初始化全部完成了,而且这样初始化的锁最后也不需要我们手动销毁 3. 互斥量的使用我们使用一把互斥锁来对我们上面的购票系统进行一下加工: 首先我们先定义一个全局的互斥量,因为多个线程都要使用,定义全局的可以提高效率 主线程对所进行初始化,然后等待子线程都执行完后,回收子线程并且销毁锁 子线程在进入临界区时加锁,出临界区时解锁 [code]#include<iostream> #include<pthread.h> #include<unistd.h> using namespace std; int count=1000; pthread_mutex_t mutex; void *GetTickets(void *args) { int thread_id = (int)(intptr_t)args; // 获取线程ID while(1) { //进入临界区时加锁 pthread_mutex_lock(&mutex); if(count>0) { usleep(10000); //模拟购票准备工作所需时间 printf("[pthread %d] get a picket, the picket number: %d\n",thread_id,count); --count; pthread_mutex_unlock(&mutex); //买完票后退出重新进入购票队列,并把锁释放 } else { //如果没票,直接退出 pthread_mutex_unlock(&mutex); break; } } return nullptr; } int main() { //创建新线程前先把锁初始化了 pthread_mutex_init(&mutex,nullptr); //创建四个新线程 pthread_t tids[4]; for(int i=0;i<4;i++) { pthread_create(&tids[i],nullptr,GetTickets,(void*)(intptr_t)(i+1)); } //阻塞等待回收线程 for(int i=0;i<4;i++) { pthread_join(tids[i],nullptr); } //线程都回收后把锁也销毁了 pthread_mutex_destroy(&mutex); return 0; }[/code]运行结果: ![]() 此时我们就发现我们最终的执行结果没有出现上面的问题了,这就是因为互斥锁保证了ticket--的原子性问题,每一次票数减一后才会有其它线程进入到临界区中 但是我们观察上面的执行结果,我们发现怎么所有的票都被线程2抢走了呢?这是因为最一开始是线程2先拿到锁抢到票,但是之后线程2对锁的竞争力就会远强于其它线程了,因为它将锁刚一释放就马上又获取了,所以我们采取的方法可以是:线程2抢完票之后可以让它短暂睡眠一会儿,这样其它线程就能够来争夺锁了 ![]() 再次运行: ![]() 此时我们就可以发现所有线程都参与到抢票中来了,符合我们的预期结果 互斥锁实现互斥的原理在了解锁的原理前,首先我们先来看一个小的知识点: 我们都知道寄存器是32位的,但是早期的寄存器实际上是只有16位的,现在的寄存器实际上可以看成两个16位的寄存器组合在一起,看成al和ah两块,al就是低位的那块,ah就是高位的那块 ![]() 锁的原理图: ![]()
![]() 下面来看一下锁的伪代码对应的各个过程: ![]() 在整个加锁的过程中只有一个数值1存在,这个1要么保存在某个线程的私有的寄存器中,要么保存在共享的互斥锁变量mutex里。如果某个线程的寄存器中的值为1,说明该线程已经加锁成功。 锁本身其实也是作为共享资源存在的,那锁本身需要保护吗? 答案是不需要,因为锁在执行各种操作时其实已经是互斥的了 互斥量的使用场景保护共享资源互斥量最常见的用途是保护共享资源,例如全局变量、文件、数据库连接等。通过加锁和解锁操作,可以确保同一时刻只有一个线程访问共享资源。 避免竞态条件互斥量可以避免竞态条件的发生。例如,在多个线程同时修改同一个变量时,使用互斥量可以确保每次修改操作是原子的。 互斥量的注意事项死锁问题死锁是指多个线程相互等待对方释放锁,导致程序无法继续执行。常见的死锁场景包括:
为了避免死锁,可以遵循以下原则:
性能开销互斥量的加锁和解锁操作会带来一定的性能开销,尤其是在高并发场景下。为了减少开销,可以:
总结互斥量是多线程编程中不可或缺的同步机制,用于保护共享资源、避免竞态条件和数据不一致问题。通过加锁和解锁操作,互斥量确保同一时刻只有一个线程访问临界区。 在实际开发中,需要根据具体场景选择合适的互斥量类型,并注意避免死锁和性能开销问题。通过合理使用互斥量,可以编写出高效、可靠的多线程程序。 除了互斥外,同步也是保证线程安全的很重要的概念,鉴于篇幅问题,同步我们放在下一篇进行讲解 本篇笔记: ![]() 感谢各位大佬观看,创作不易,还请各位大佬点赞支持!!! 免责声明:本内容来源于网络,如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |