一、线程互斥的概念
1.1临界资源与互斥的关系
临界资源:多线程执行流共享的资源就叫做临界资源。
临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。
1.2互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,会带来一些问题。
以下面代码为例,创建四个线程同时进行抢票,如果多个线程同时对一个临界资源进行访问而该资源却没有被保护,就会出现bug。
// 操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void* route(void* arg)
{char* id = (char*)arg;while (1) {if (ticket > 0) {usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;}else {break;}}
}
如上图所示,票到最后被枪成了负数而其原因就是代码中没有对临界区资源进行保护,加上临界区对临界资源的操作也并不是原子的,所以最终导致票数成了负数。
1.3汇编角度分析
取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>
-- 操作并不是原子操作,而是对应三条汇编指令:
load :将共享变量ticket从内存加载到寄存器中
update : 更新寄存器里面的值,执行-1操作
store :将新值,从寄存器写回共享变量ticket的内存地址。
而线程是随时都有可能被切换的,依照如上代码的逻辑,临界区的操作并不是原子的,CPU先将ticket从内存中取出来放到寄存器中,然后对其进行减法操作,最后再将其放回到内存当中,其中任何一步都有可能导致线程带着上下文数据被切出而内存中ticket的值依然没有改变,导致其他线程进行访问操作时依然拿到的是未被改变的ticket值,然后继续往下执行,而另一个线程此时又将更改过的ticket放回到内存中,此时就会导致tickt的值比规定的下限值要低。
要解决以上问题,需要做到三点:
1.代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
2.如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
3.如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。 Linux 上提供的这把锁叫互斥量。
二、互斥量(锁)的接口
2.1初始化互斥量
初始化互斥量有两种方法:
加锁本质上就是将并行执行变为串行执行。
方法1,静态分配:
如果要定义一把静态的锁,或者是全局的锁,直接定义一个pthread_mutex_t类型的变量,然后通过PTHREAD_MUTEX_INITIALIZER进行初始化就可以使用锁。
pthread_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
2.2申请及使用锁
加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
申请锁会出现以下几种状态:
1、申请成功:函数就会返回,允许继续运行。
2、申请失败:函数就会阻塞,不允许继续往下执行。
3、函数调用失败,出错返回。
解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
尝试申请锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
三、互斥量实现原理
经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下。
int ticket = 100;
pthread_mutex_t mutex;
void* route(void* arg)
{char* id = (char*)arg;while (1) {pthread_mutex_lock(&mutex);if (ticket > 0) {usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;pthread_mutex_unlock(&mutex);// sched_yield(); 放弃CPU}else {pthread_mutex_unlock(&mutex);break;}}
}
通俗来讲:调用锁以后,CPU内会存在一个al寄存器内部存储一个0,在申请锁时,内存中存在mutex内部为1,在一个线程调用锁时,CPU会将al和mutex中的值直接交换,如果此时al为1则说明申请成功,而该线程被中断后会将CPU寄存器中自己的上下文数据全部带走,此时al和mutex中都是0,当下一个线程申请锁时,al交换到的mutex中的内容还是0,此时就只能挂起等待别的线程将锁归还。