一、线程互斥
引入
要理解线程互斥的概念,首先我们要先从为什么线程需要互斥这个点切入。
我们先来看看下面这串代码,这是一段有关抢票的代码。
运行结果:
我们让三个线程同时进行抢票,发现票数被抢到了负数,这样的结果当然不符合我们的预期。
票数抢到负数是因为线程产生了竞态关系,当一个线程达到 0 时,另一个线程可能刚好执行了 -- 的代码,此时当前的线程在进行 -- 就出现了负数。
当多个线程访问共享资源时,如果不考虑线程安全问题,可能会导致数据丢失、错误等原因。
1. 进程互斥基本概念与背景
(1)互斥与并发
互斥就是相互排斥,有你没我,与互斥相对的一组是并发,并发就是可以同时存在。
我们将这组概念带入线程中,互斥就是在同一时间,只能有一个线程对共享资源进行访问;线程并发就是在同一时间,线程同时竞争共享资源。
(2)同步与异步
同步与异步是相对的,同步必须等待被调用方执行完毕并返回结果才能继续执行后续操作,相当于线程被阻塞住了。异步就是多个线程自己执行自己的内容,不需要关心其他的线程。
(3)操作原子性
原子性操作是指当一个线程执行一段代码的时候,此时不能切换到其他线程,必须执行完当前线程的代码才能进行切换。
(4)临界区和临界资源
临界资源是指在多线程环境下,不能被线程同时访问和使用的资源,即被使用的共享资源就是临界资源。临界区指访问临界资源的代码块,从起点到终点,这块地方就是临界资源。
为了保护这些临界资源,操作系统提供了一些机制。给临界资源进行上锁,使用信号量进行访问控制等等。
2. 互斥量 mutex
(1)案例解析
对于上述的抢票代码,票数产生了负数。在线程代码中 usleep 进行了一段休眠,这会导致很多线程进入该代码段,而且 ticket-- 本身就不是一个原子操作,所以说,要解决上述的问题,我们就要对临界区进行保护,于是我们便有了锁,用来对临界资源进行上锁,使得在临界区当中,每次运行只允许一个线程执行。

(2)系统接口
操作系统给出了互斥锁(mutex)用于解决当前情况。它可以保证在相同时间下,只有一个线程可以访问共享资源或代码段。互斥锁的用法也很简单,首先定义一个互斥锁对象,在临界区处进行上锁,在临界区末尾释放锁。线程拿到锁进行原子操作,执行完临界区代码后会释放锁,其他的线程会在临界区外部等待锁,只有拿到了锁才能进入临界区进行访问。这样使得临界区每次进行访问只有拥有锁的线程才能进行访问,保证了线程执行的安全性。
下面我们来介绍一下 POSIX 线程库封装的一些线程互斥接口。
POSIX 线程库中用于操作互斥量的函数都以 pthread_mutex 打头:
初始化互斥量:
静态初始化互斥量:pthread_mutex_t
静态初始化的互斥量不需要进行 destroy 操作,程序结束会自动回收
动态初始化互斥量:pthread_mutex_init
参数:
mutex:指向类型变量的指针
mutexattr:给 mutex 设置属性,我们一般使用 nullptr 默认值
销毁互斥量:
动态分配后的互斥锁若不使用需要进行回收
pthread_mutex_destroy:
参数:
mutex:指向类型变量的指针
互斥量加锁和解锁:
对临界区资源进行加锁解锁,当互斥量处于未锁状态,该函数会将互斥量锁定,若其他线程已经锁定该互斥量,那么该执行流会被阻塞挂起,等待互斥量解锁
pthread_mutex_lock:
pthread_mutex_unlock:
参数:





