🍎作者:阿润菜菜
📖专栏:Linux系统编程
目录
- 一、线程互斥
- 1. 为什么要有共享资源临界保护?
- 2.理解加锁
- 2.1 认识锁,使用锁
线程同步互斥问题是指多线程程序中,如何保证共享资源的正确访问和线程间的协作。
因为线程互斥是实现线程同步的基础和前提,我们先讲解线程互斥问题。
一、线程互斥
1. 为什么要有共享资源临界保护?
在多线程中,假设我们有一个黄牛抢票的代码,其中有一份共享资源tickets,如果多个线程都在抢票也就是对这个全局变量tickets做–操作,如果我们没有对共享资源做保护(同一时间只能一个线程对资源进行访问)的话,就会存在并发访问的问题,进而导致数据不一致问题!这种情况下,票数最后会出现负数的情况。
那为什么会出现并发访问导致数据不一致问题呢?–
了解上面的问题需要知道线程调度的特性,实际线程在被调度时他的上下文会被加载到CPU的寄存器中,而线程在被切换的时候,线程又会带着自己的上下文被切换下去,此时要进行线程的上下文保存,以便于下次该线程被切换上来的时候能够进行上下文数据的恢复。
除此之外,像tickets- -这样的操作,对应的汇编指令其实至少有三条,1.读取数据 2.修改数据 3.写回数据,而线程函数我们知道会在每个线程的私有栈都存在一份,在上面的例子中多个线程执行同一份线程函数,所以这个线程函数就绝对会处于被重入的状态,也就绝对会被多个线程执行!今天我们假设只有一个CPU(CPU就是核心,处理器芯片会集成多个核心)在调度当前进程中的线程,那么线程是CPU调度的基本单位,所以也就会出现一个线程可能执行一半的时候被切换下去了,并且该线程的上下文被保存起来,然后CPU又去调度进程中的另一个线程。
当多个线程同时进入到分支判断语句,然后去阻塞等待的情况,假设tickets已经变成了1,然后其余的线程此时都被调度上来了,他们都开始执行tickets- -,- -之后不满足循环条件线程才会退出,那么如果我们创建出了4个线程,就会有3个线程在票数已经为0的情况下继续减减,所以就会出现票数为负数的情况
要解决以上的问题,我们提出的解决方案就是:加锁
在学习锁之间先搞清两个概念:
临界资源是指一次仅允许一个进程或线程使用的共享资源,如文件、变量等。
临界区是指每个进程或线程中访问临界资源的那段代码,需要保证互斥和同步的执行。
临界资源和临界区的区别是:
- 临界资源是一种系统资源,需要不同进程或线程互斥访问,而临界区则是每个进程或线程中访问临界资源的一段代码,是属于对应进程或线程的。
- 临界资源是一种抽象的概念,表示需要保护的共享数据或设备,而临界区是一种具体的实现,表示访问临界资源的具体操作和逻辑。
- 临界资源是一种静态的属性,表示某种资源是否可以被多个进程或线程同时使用,而临界区是一种动态的状态,表示某个进程或线程是否正在使用某种临界资源。
2.理解加锁
2.1 认识锁,使用锁
如果我们想让多个执行流串行的访问临界资源,而不是并发或并行的访问临界资源,这样的线程调度方案就是互斥式的访问临界资源!(串行就是指只要一个线程开始执行这个任务,那么他就不能中断,必须得等这个线程执行完这个任务,你才能切换其他线程执行其他的任务)
加锁后线程的操作是原子性的,怎么理解?
当线程在执行一个对资源访问的操作时,要么做了这个操作,要么没有做这个操作,只要两种状态,不会出现做了一半这样的状态,我们称这样的操作是原子性的。(就比如你妈让你写作业,你要么给我把作业写完了再出去玩,要么就一个字也别写给我滚出家门,就这两种状态,不会出现你写了一半,然后你妈让你出去玩的这种情况,这样也是原子性)
我们下面讲解互斥锁
首先锁实际就是一种数据类型
,这个锁就像我们平常定义出来的变量或是对象一样,只不过这个锁的类型是系统给我们封装好的一种类型,进行重定义后为pthread_mutex_t。变量或对象在生命的时候也是可以初始化的,变量初始化后,就是变量的定义,而不是声明了。变量和对象也都有自己的销毁方案,内置类型的变量销毁时,操作系统会自动回收其资源,而自定义对象销毁时,操作系统会调用其析构函数进行资源的回收。
锁同样也是如此,锁也有自己的初始化和销毁方案,如果你定义的是一把局部锁,就需要用pthread_mutex_init()和pthread_mutex_destroy()来进行初始化和销毁,如果你定义的是一把全局锁或静态所,则不需要用init初始化和destroy销毁,直接用PTHREAD_MUTEX_INITIALIZER进行初始化即可,他有自己的初始化和销毁方案,我们无须关心静态或全局锁如何销毁。
定义好锁之后,我们就可以对某一段代码进行加锁和解锁,加锁与解锁意味着,这段代码不是一般的代码,只有申请到锁,持有锁的线程才能访问这段代码,加锁和解锁之间的代码可以称为临界区,因为想要访问这段空间必须有锁才可以访问。
那我们该如何对共享资源进行加锁和解锁呢?
手册这样写的:
加锁的使用方法一般包括以下几个步骤:
- 创建并初始化一个加锁原语对象,使用相应的API来分配内存并设置属性,如
pthread_mutex_init
用于创建并初始化一个互斥锁对象。 - 在访问共享资源或临界区域前,对加锁原语对象进行加锁操作,使用相应的API来获取锁的所有权,如
pthread_mutex_lock
用于以阻塞方式获取一个互斥锁。 - 在访问共享资源或临界区域后,对加锁原语对象进行解锁操作,使用相应的API来释放锁的所有权,如
pthread_mutex_unlock
用于释放一个互斥锁。 - 在不需要使用共享资源或临界区域时,销毁加锁原语对象,使用相应的API来释放内存并清理资源,如
pthread_mutex_destroy
用于销毁一个互斥锁对象。
如果忘记解锁,即一个线程在获取一个锁后,没有正确地释放锁,而导致其他线程无法获取该锁。为了避免这种情况,可以使用以下方法:
- 使用RAII技术,即将加锁和解锁操作封装在一个类中,在构造函数中加锁,在析构函数中解锁,这样可以利用对象的生命周期来自动管理锁的状态。
- 使用异常处理机制,即在加锁和解锁操作之间使用try-catch语句来捕获可能抛出的异常,并在catch块中进行解锁操作,这样可以避免异常导致的忘记解锁。
未完待续