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