加锁保护
我们上一篇提到过,多个线程执行下面代码可能会出错,具体原因可查看上一篇Linux博客。
为避免这种错误的出现,我们可采用加锁保护。
互斥锁
PTHREAD_MUTEX_INITIALIZER
用pthread_mutex_t定义一把锁。ptherad_mutex_init是对锁进行初始化的函数。如果这把锁是全局的并且是静态定义的,我们可直接使用PTHREAD_MUTEX_INITIALIZER这样的话宏进行初始化,用这种方式进行初始化不需要init和destroy。
在这里,这把锁用来保护全局的tickets这一过程我们称为访问临界资源时对临界资源进行保护。
访问临界资源的代码被称作临界区,这里的临界区是函数里面if语句大括号里包含的内容
pthread_mutex_lock
返回值成功返回0,失败返回错误码。
pthread_mutex_lock是进行加锁,出入的是刚才定义的锁
加了锁之后,每个线程都会进行加锁,加锁的特点是:任何一个时刻,只允许一个线程成功获得这把锁,其它没有拿到锁的线程进行阻塞等待,直到拿到锁的线程把锁给释放掉,其它线程才能进来。
加锁之后,任何一个时刻,只允许一个线程执行这部分代码以及后面的解锁。即加锁后的代码只允许一个线程去执行,其它线程进行阻塞等待。
pthread_mutex_unlock
pthread_mutex_unlock是用来进行解锁的,返回值成功返回0,失败返回错误码。
若把解锁写在这里,当tickets为0时候,执行break,此时直接退出while循环,未经过解锁,其它线程无法会一直阻塞
因此,我们需要修改加锁位置,其中加锁和解锁之间的代码称为临界区,这里被共享访问的tickets就是临界资源。
运行程序后,结果正确,但我们可以看到截图里是一个进程在抢,这是因为当某个线程抢完票后,释放了所,但该线程的优先级可能很高,进而又被调度会又被执行,
我们让程序执行完后usleep一会
此时可以获得我们想要的结果
加锁的时候,一定要保证加锁的力度,越小越好,越细越好。如这里,尽量不要像下图这样加解锁,这段程序我们主要的核心还是tickets,这里我们可以把函数里面的打印语句,放到解锁后面,
ptherad_mutex_init
若我们不想定义全局锁(全局变量的锁),若我们在main函数定义了一把锁,若要使用,就必须用pthread_mutex_init进行初始化。
第一个参数,这把锁的地址,第二个参数,这把锁的属性,我们一般设为空。
返回值成功返回0,失败返回错误码。
pthread_mutex_destory
当我们不要在main函数定义的锁时,我们用pthread_mutex_destory
我们用局部的锁,对多个线程进行加锁
进行一下小优化
思考题
加了锁之后,线程在临界区中,是否会切换,会有问题吗?
会切换,OS说了算,OS调度器决定是否切换。不会有问题,因为虽然被切换了,但是我们是持有锁被切换的,所以其它抢票线程要执行临界区代码,也必须先申请锁,锁它是无法申请成功的,因为锁在被切走的进程身上,所以,也不会让其它线程进入临界区,就保证了临界区中数据一致性。当一个线程,不申请锁,直接去访问临界资源,这种方式是错误的编码方式。
原子性在哪里体现?
目前来说,在没有持有锁的线程看来,对该线程最有意义的情况只有俩种:1.前面的线程未持有锁(什么都没做)2.前面的线程释放锁(做完)。因为此时该线程可以申请锁。
加锁就是串行执行了吗?
是的,执行临界区代码一定是串行的。
要访问临界资源,每一个线程都必须先申请锁,前提是每一个线程都必须看到同一把锁并且并且去访问它,锁本身就是一种共享资源。在上面代码中,锁保护了tickets,而谁又来保证锁的安全呢?所以,为了保证锁的安全,申请和释放锁必须是原子的。那么有如何保证原子性呢?我们继续往下看,锁是如何实现的。
互斥锁的实现
swap或exchange指令是以一条汇编的方式,将内存和CPU内寄存器数据进行交换。
如果我们在汇编的角度,如果只有一条汇编语句,我们就人为该汇编语句的执行是原子的。
加锁和解锁的代码
lock相当于pthread_mutex_lock(),unlock也一样
lock过程:
在进程或线程角度,是如何看待CPU上寄存器的,CPU内部的寄存器,本质叫做当前执行流的上下文。寄存器的空间是被所有的执行流共享的,但是寄存器的内容是被每一个执行流私有的,因为这些是当前执行流的上下文。
%al是CPU中的一个寄存器,一开始把0放到该寄存器中,0虽然被放到了寄存器中,但0是某个线程的上下文,接下来进行交换xchgb,该语句只有一句,所以该语句是原子性的,这里的含义是把寄存器的值和锁的值做交换。交换玩之后去判断寄存器内容,如果>0,就返回,即锁申请成功,反之阻塞挂起。
该图右边是内存,mtx是锁。
当执行完第一条语句之后,线程有可能被切换,线程被切走的时候是带着数据走的,走的时候带走了寄存器里放进去的0,当新线程来了之后,新线程照样从第一句语句开始执行(这是加锁操作必须的),新线程把0放了进来,也就是说此时俩个线程都有0,进而说明,寄存器是共享的,但寄存器里面的内容是私有的。
当第二个线程执行完第二条语句后,此时线程被切走,寄存器里现在是1,mtx里面是0,第二个线程被切走的时候要带走自己的上下文,即把1带走,之后第一个线程被切了回来,第一个线程走的时候带的是0,第一个线程回来之后在寄存器里面放了0,之后第一个线程继续执行语句,由于走的时候第一条语句已经执行完了,现在要执行第二条交换语句(此时内存里mtx是0),交换之后,寄存器里和mtx都是0(0和0交换),第一个线程做判断,发现寄存器里的值是0,这时候就把第一个线程挂起,挂起的时候第一个线程又把上下文数据0带走了,这时候恢复第二个线程,把1恢复到寄存器,由于走的时候完成了交换,所以现在进行判断操作,寄存器里内容>0,直接返回,此时申请锁成功,lock调用被返回pthread_mutex_lock也被返回,之后继续执行后续的代码。
交换这一行是真正的申请锁。这里1被所有的线程轮流式的拿到,而且这个数字1永远只有1个,而且
不会有任何新增的数据存在,这个1就是锁。
谁来保证锁的安全?
自己保证,通过一行汇编的方式来保证原子性。
锁的实现就是上面所述内容:简单来说通过内存数据和CPU寄存器数据进行交换。
可重入VS线程安全
可重入是针对函数来说的,一个函数被多个执行流重复进入的现象叫可重入,在重入期间如果该函数没问题,该函数就叫可重入函数。
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
常见的线程不安全的情况
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
常见的线程安全的情况
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
常见可重入的情况
不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的(例如抢票成程序,每个线程都在对全局变量做修改)。
可重入与线程安全区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
注意:如果一个函数是可重入的,它一定是线程安全的,如果是线程安全的,但不一定可重入。
死锁
我们在实际中不可能只用一把锁,我们可能会用多把锁,这里以俩把锁为例。
线程A要完成某些工作,申请锁1和锁2。线程B也需要俩把锁,先申请锁2,再申请锁1。我们单个申请一把锁是原子的,当一把锁申请完再去申请另一把锁,可能会出问题,如这里的线程A,B,俩个同时运行线程A拿锁1,线程B拿锁2,接下来线程A申请锁2,线程B申请锁1,此时就会出现问题,双方想要的锁都被对方拿到。此时出现了互相申请对方锁的情况,这种情况就叫死锁。
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。当只有一把锁的时候,也有可能会产生死锁。
当我们把抢票程序里面解锁的地方,改为申请锁
此时程序卡在了这里
死锁四个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配
避免死锁算法
死锁检测算法(了解)
银行家算法(了解)
pthrad_mutex_trylock
pthread_mutex_trylock() 是 pthread_mutex_lock() 的非阻塞版本。如果 mutex 所引用的互斥对象当前被任何线程(包括当前线程)锁定,则将立即返回该调用。否则,该互斥锁将处于锁定状态,调用线程是其属主。文章来源:https://www.toymoban.com/news/detail-449771.html
pthread_mutex_trylock() 在成功完成之后会返回零。其他任何返回值都表示出现了错误。如果出现以下任一情况,该函数将失败并返回对应的值。文章来源地址https://www.toymoban.com/news/detail-449771.html
到了这里,关于Linux——线程3|线程互斥和同步的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!