文章目录
线程互斥
一些背景知识
在讨论线程互斥的话题之前,我想先和大家说明一下下面的几个概念
- 共享资源:在我们前面讨论线程的时候,我们知道线程的绝大多数资源都是共享的,我们把这些多执行流可以共享的资源叫作共享资源;共享资源不会导致数据不一致问题,只有访问了共享资源才有可能导致数据不一致问题
- 临界资源:如果我们把多执行流的共享资源保护起来,那么被保护起来的共享资源就叫临界资源,如管道
- 临界区:在每个线程内部,我们把访问临界资源的那一部分代码叫作临界区
- 互斥:在任何时刻,只允许一个执行流进入临界区访问临界资源就叫做互斥
- 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
互斥量
共享资源的并发访问问题
在谈论互斥量之前,我想先带领大家思考一个问题,如果一个共享资源没有被保护,会存在什么问题?我们来看下面的一个代码:
cpp
#include<iostream>
#include<pthread.h>
#include<unistd.h>
int count=100;
void*ThreadRoutine(void*arg)
{
char*name=(char*)arg;
while(true)
{
if(count>0){
usleep(1000);
printf("%s线程开始执行,count的值为:%d\n",name,count);
count--;
}
else{
break;
}
}
return (void*)0;
}
int main()
{
pthread_t t1,t2,t3,t4,t5;
pthread_create(&t1,nullptr,ThreadRoutine,(void*)"thread-1");
pthread_create(&t2,nullptr,ThreadRoutine,(void*)"thread-2");
pthread_create(&t3,nullptr,ThreadRoutine,(void*)"thread-3");
pthread_create(&t4,nullptr,ThreadRoutine,(void*)"thread-4");
pthread_create(&t5,nullptr,ThreadRoutine,(void*)"thread-5");
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
pthread_join(t4,nullptr);
pthread_join(t5,nullptr);
return 0;
}
运行结果如下所示:

我们看到,此时竟然出现了负数,那么此时我们就有一个问题:为什么会出现减到负数的情况?
首先我们来看上面的代码,我们可以看到,无论是全局变量还是局部变量,最终都会在物理内存中存储,而我们对全局变量--和判断的操作最终都会在cpu上进行运算,也就是说,对于算数和逻辑问题,都会由CPU帮我们完成,而数据却在内存当中,所以注定了一次运算会有三个不同的阶段:
- 取指令:将数据从内存当中加载到CPU内部
- 分析指令:是逻辑运算还是算数运算
- 执行指令:进行计算
也就是说,站在硬件的角度,CPU为了能够完成计算,需要把指令和数据拷贝到CPU内部的寄存器里面,然后在CPU内部进行算数运算和逻辑运算,而CPU为了完成运算的过程必定会以进程或线程为载体执行;站在操作系统的角度,把数据从内存搬迁到寄存器时,内存中的数据属于共享资源,寄存器属于硬件上下文,而线程要进行切换时需要保存自己的上下文数据,但是寄存器只有一份,但寄存器里面的数据每个线程都有一份;
换句话说,当我们把数据从内存拷贝到寄存器当中,CPU必定在执行某一个线程对应的代码,同时也是把该变量的内容变成私有化内容
如果我们存在两个线程A和B,假设线程A正在执行逻辑判断,满足逻辑判断后进入到代码里面,如果此时线程A的时间片到了,那么线程A就需要被切换,但是线程A会保存自己的硬件上下文;接着线程B开始执行,线程B也开始执行逻辑判断,判断为真,进入到代码逻辑里面,如果此时线程B把全局变量减到了0时间片到了,此时线程A重新开始运行,线程A此时的硬件上下文表示全局变量还没有减到0,因此接着上次的代码继续向后运行,那么此时线程A对全局变量进行减一时就会把全局变量减到负数,由此我们就解释了为什么count会被减到负数,也就是说,因为if判断的存在,所以导致了全局变量count被减到了负数
那么此时我们还有一个问题,对于全局变量进行--的操作是安全的吗?
3这里我们只看到count--着一条代码,但实际上,count--这句代码在编译成汇编代码的时候是被编译成了三条语句
- 第一步,将内存中的变量拷贝到cpu寄存器当中
- 第二步:执行--操作
- 第三步:将结果写回到内存中
那么有没有可能,我们在执行其中一步操作的时候时间片到了,然后A线程的上下文被保存,同时将B线程切换进来,假设线程B将count减到了0,然后线程A被重新切换回来,此时线程A继续在原来被切换的位置继续向下执行,并把count的值又变成了非0的值,此时可能会出现重复的值
综上所述,在多线程的场景下,对全局变量进行并发访问本身不是线程安全的
问题反思引出概念
对于上面的代码,我们来分析一下上面的背景知识里面所提到的概念:
- 共享资源:就是count全局变量
- 临界资源:如果我们把count保护起来,那么count就变成了临界资源
- 临界区:访问count变量的代码
为了解决共享资源的并发访问问题,我们就需要把共享资源保护起来,形成临界区,为了保护共享资源,pthread库提出了互斥锁的概念
互斥锁
pthread库所定义的互斥锁是用pthread_mutex_t这个类型去定义的
定义互斥锁:
- 静态分配
如果是定义的是静态或全局的,可以使用PTHREAD_MUTEX_INITALIZER去初始化 - 动态分配
如果我们的互斥锁是在栈上开辟的,可以使用pthread_mutex_init这个函数去初始化

- 我们可以使用
pthread_mutex_destroy这个函数去销毁,如果我们定义的是全局的互斥锁,那么就不需要销毁,如果我们定义的是局部的,那么就需要调用pthread_mutex_destroy去销毁
我们用上面的互斥锁对上面的代码进行加锁解锁的操作:
cpp
#include<iostream>
#include<pthread.h>
#include<unistd.h>
//全局的锁
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
int count=100;
void*ThreadRoutine(void*arg)
{
char*name=(char*)arg;
while(true)
{
pthread_mutex_lock(&mutex);
if(count>0){
usleep(1000);
printf("%s线程开始执行,count的值为:%d\n",name,count);
count--;
pthread_mutex_unlock(&mutex);
}
else{
pthread_mutex_unlock(&mutex);
break;
}
}
return (void*)0;
}
int main()
{
pthread_t t1,t2,t3,t4,t5;
pthread_create(&t1,nullptr,ThreadRoutine,(void*)"thread-1");
pthread_create(&t2,nullptr,ThreadRoutine,(void*)"thread-2");
pthread_create(&t3,nullptr,ThreadRoutine,(void*)"thread-3");
pthread_create(&t4,nullptr,ThreadRoutine,(void*)"thread-4");
pthread_create(&t5,nullptr,ThreadRoutine,(void*)"thread-5");
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
pthread_join(t4,nullptr);
pthread_join(t5,nullptr);
return 0;
}
此时我们来看这个代码,我们有下面几个问题:其中的互斥锁不就是全局的吗?怎么保证锁的安全?所有的线程都需要申请锁吗?如果线程进入到临界区,会因为切换导致并发问题吗?
首先来看问题一:实际上这个互斥锁是原子的,不用担心其他线程去抢占它,具体原因我们放在之后进行分析
对于第二个问题:不能所有线程都申请锁,但是如果要加锁必须加同一把锁,如果加锁了,所有线程在访问共享资源的时候都要严格遵守先加锁再解锁的逻辑,不存在并发问题
对于第三个问题:如果对临界区加锁了,线程进入到临界区,也会被切换,但是不会因为切换而导致并发问题
而我们加锁之后,访问临界区就变成了原子的