1.互斥相关背景和概念:

共享资源:所有线程都能看到的资源叫做共享资源,例如全局变量。

临界区:每个线程内部访问临界资源的代码叫做临界区。

互斥:任何时刻,互斥保证有且只有一个执行流进入临界区访问该临界资源,对临界资源起保护的作用。

原⼦性:不会被任何调度机制打断的操作,该操作只有两态,要么完成, 要么未完成。
  2.互斥量mutex
  mutex是一个互斥量,用来保证 共享资源的安全,使多个线程互斥。多个线程就类似多个打印任务,如果不加保护同时使用打印机进行打印,那么势必会出现打印错乱的一些情况,这时候就需要使用互斥锁,这是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。
这里使用一个抢票例子来说明不加锁的多线程会出现的问题:
  这里使用四个线程来模拟多线程抢票的过程,结果却出现了负数,本应该大于0才进行抢票,但是怎么会出现负数呢?这就是没有对该临界区加锁导致的后果。if 语句判断条件为真以后,代码可以并发的切换到其他线程usleep 这个模拟漫⻓业务的过程,在这个漫⻓的业务过程中,可能有很多个线程会进⼊该代码段,其他语句也判断为真也会休眠然后进行--操作,此时假如ticketnum已经为1了,但是四个进程同时通过了if判断,然后都进行--操作,由于--操作并不是原子的,是由三个步骤构成:将数据从内存搬入cpu,在cpu内部进行-1操作,再从cpu将数据写会内存中,在这个过程中也其他线程搬入cpu的数据可能还没有修改所以最后会出现两个相同的-1值以及小于0的结果。
要解决以上问题,需要做到三点:
代码必须要有互斥⾏为:当代码进⼊临界区执⾏时,不允许其他线程进⼊该临界区。
如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程
进⼊该临界区。
如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区。
要做到以上的三点那么就需要一把锁,这个锁叫做互斥量。

【互斥锁的特点】:

1. 原子性:把一个互斥量锁定为一个原子操作,这意味着操作系统(或pthread函数库)保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量;

2. 唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;

3. 非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。

  2.2互斥量的接口

初始化互斥量有两种方法:

方法1,静态分配,适用于全局变量

phtread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法2,动态分配,适用于局部变量

int pthread_mutex_init ( pthread_mutex_t * restrict mutex, const
pthread_mutexattr_t * restrict  attr);
参数:
mutex :要初始化的互斥量
attr NULL
返回值:成功返回0,失败返回错误

销毁互斥量:

使⽤ PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁 , 不要销毁⼀个已经加锁的互斥量 , 已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁。
int pthread_mutex_destroy ( pthread_mutex_t *mutex)
互斥量加锁和解锁
int pthread_mutex_lock ( pthread_mutex_t *mutex);
int pthread_mutex_unlock ( pthread_mutex_t *mutex);
返回值 : 成功返回 0 , 失败返回错误号
调⽤ pthread_ lock 时,可能会遇到以下情况:
(1)互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
(2)发起函数调⽤时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调⽤会陷⼊阻塞(执⾏流被挂起),等待互斥量解锁。
修改上面的抢票系统:在临界资源加上一把锁并在不使用时解锁。
现在就可以正确进行抢票了
  3.互斥量实现的原理
  
  为了实现互斥锁的操作,大多数体系结构都提供了swap和exchange指令,该指令的作用时将寄存器数据和内存单元的数据交换,只有一条指令,保证了原子性,防止其他线程抢占资源。
Lock和unloc的伪代码:
  解释一下,%al是一个寄存器,在刚进入lock函数时会将0写入此线程的一个寄存器中,然后将寄存器的值与mutex互斥量进行交换,由于这是原子操作,所以无法被打断,即该线程上锁就不会被其他线程抢占锁了,此时寄存器大于0就会return 0然后继续执行临界区的代码,如果此时线程被切换,其他线程调用lock函数时,mutex此时已经为0,寄存器交换以后的值还是0,所以会被挂起等待,锁在线程切换时就已经被上下文保护起来了,其他线程并不能够访问。当上锁的线程执行unlock时会将锁归还给mutex,然后唤醒其他正在阻塞的线程,此时正在等待的其他线程会再次竞争互斥锁的资源。
  4.互斥量的封装
  这里对系统的互斥量进行了封装,并且使用rall风格进行调用。
  调用时只需要保证LockGuard创建的每次保存的都是同一把锁即可:
  这样就完成了对互斥量的简单封装并使用RALL风格进行管理,调用者只需要创建对象不需要进行加锁解锁的操作。
  5.总结:本章对互斥量背景,概念,原理进行了讲解,并使用抢票例子进行说明互斥在多线程中的重要性,希望以上内容对你有所帮助。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐