作者: 主页
我的专栏 C语言从0到1 探秘C++ 数据结构从0到1 探秘Linux 菜鸟刷题集 欢迎关注:点赞收藏✍️留言
码字不易,你的点赞收藏❤️关注对我真的很重要,有问题可在评论区提出,感谢阅读!!!
当谈到多线程编程时,线程互斥是一个至关重要的概念。在多线程环境下,确保共享资源的安全访问是至关重要的,而线程互斥正是为此而设计的。通过线程互斥,我们能够确保在任意给定时间内,只有一个线程能够访问共享资源,从而避免竞态条件和数据损坏。
在本篇博客中,我们将探讨线程互斥的重要性、实现线程互斥的方法以及在实际编程中如何应用线程互斥来确保多线程程序的正确性和稳定性。通过深入了解线程互斥,我们可以更好地理解多线程编程中的关键概念,提高程序的可靠性和性能。
希望本篇博客能够帮助你更好地理解线程互斥,并为你在多线程编程中遇到的挑战提供一些思路和解决方案。让我们一起深入探讨线程互斥,为构建高效、稳定的多线程程序打下坚实的基础。
多个线程并发同一段代码时不会出现不同的结果,多个执行流访问临界资源不会导致程序产生二义性。
不安全、和上面就相反喽
- 假设一个场景:
假设有一个CPU,两个线程,线程A和线程B,线程A和线程B都要对全局变量i(10)进行++操作- 假设线程A先运行,但是线程A将i的值读取到寄存器之后,就被线程切换了。(操作系统会保存线程A的程序计数器和上下文信息)
- 假设B线程运行,正常继续++操作,那么i的值在内存中就被修改增加1了
- 此时线程A切换回来了,怎么计算?内存中i的值是多少?
- 结论:此时的i最终结果还是11,明明加了两次,但是却不符合逻辑,这就是不安全
- 怎么解决?这就需要用到互斥了,每次只允许一个线程进入修改,这样就不会有这种情况了
在我们想买票的时候,如果有两个人同时下单,它会发生什么呢?票是怎么发放的,会不会有两个人买到同一张票的情况?会不会有票数为负的情况?
我们通过代码来模拟一下,如果线程不安全时,抢票的情况
代码如下:
我们让4个线程来循环获取ticket,模拟抢票
#include
#include
#include
using namespace std;
int ticket=1000;
void* get_ticket(void* arg)
{
while(1)
{
if(ticket>0)
{
cout<<"i am "<<pthread_self()<<" get a ticket,no:"<<ticket<<endl;
ticket--;
}
else
{
break;
}
}
return NULL;
}
int main()
{
pthread_t tid[4];
for(int i=0;i<4;i++)
{
int ret=pthread_create(&tid[i],NULL,get_ticket,NULL);
if(ret!=0)
{
cout<<"线程创建失败!"<<endl;
}
}
for(int i=0;i<4;i++)
{
pthread_join(tid[i],NULL);
}
cout<<"pthread_join end!"<<endl;
return 0;
}
结果如下:
可以看到出现了负数的情况
甚至出现了两个线程抢到同一张票的情况
这就是所谓的线程不安全的情况
之前的抢票模拟,可以看到出现了负数的票,但是我们的条件中清楚的要求>0,这是为什么?
取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34
要解决上面的问题需要做到三点
拿之前的抢票的情况来说,如果加上这个锁,当一个线程想去访问临界资源,他得先获取互斥锁,如果此时互斥锁的值为1,则说明它可以访问,反之则不能,如果它正在访问临界资源,此时有第二个线程想来访问临界资源,发现互斥锁为0,它就不能进入,只能等待互斥锁为1时才能进入访问。这就保证当前的临界资源在同一时刻只能被一个执行流访问了。
但是需要注意的是,如果多个线程访问临界资源的时候是互斥访问的属性,一定要在多个线程中进行同一把锁的加锁操作,这样每个线程在访问临界资源之前都要获取这把锁,若锁的值为1就可以访问,反之则不能访问;如果给线程A加锁,但是不给线程B加锁,就会导致线程不安全的情况。
加锁的时候会提前在寄存器的计数器中保存的一个值 0,而不管内存的计数器中保存的值为多少,都会将寄存器中保存到值 0 和内存计数器中保存的值进行交换,然后对寄存器中的值进行判断是否为 1 ,如果为 1 ,则能加锁,如果不为 1 ,则不能加锁。
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令
该指令的作用是把寄存器和内存单元的数据相交换
由于只有一条指令,保证了原子性
即使是多处理器平台,访问内存的总线周期也有先后
一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
现在我们把lock和unlock的伪代码给一下。
初始化互斥量有两种方法:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrictattr);
返回值及参数说明:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
struct timespec{
time_t tv_sec;//秒
long tv_nsec;//纳秒
};
int pthread_mutex_unlock(pthread_mutex_t *mutex);
先初始化互斥锁,再创建线程
在所有使用互斥锁的线程全部退出之后就可以销毁互斥锁
线程访问临界资源之前进行加锁操作
线程所有退出的地方进行解锁
以之前的抢票为例
#include
#include
#include
using namespace std;
int ticket=1000;
pthread_mutex_t g_lock; //全局变量的互斥锁
void* get_ticket(void* arg)
{
while(1)//1位置加锁还是在2位置加锁
{
pthread_mutex_lock(&g_lock);
//pos1
if(ticket>0)
{
cout<<"i am "<<pthread_self()<<" get a ticket,no:"<<ticket<<endl;
//pos2
ticket--;
}
else
{
//将下面解锁的注释去掉就是正确的代码
//pthread_mutex_unlock(&g_lock);
break;
}
//pthread_mutex_unlock(&g_lock);
}
return NULL;
}
int main()
{
pthread_mutex_init(&g_lock,NULL);//初始化互斥锁
pthread_t tid[4];
for(int i=0;i<4;i++)
{
int ret=pthread_create(&tid[i],NULL,get_ticket,NULL);
if(ret!=0)
{
cout<<"线程创建失败!"<<endl;
}
}
for(int i=0;i<4;i++)
{
pthread_join(tid[i],NULL);
}
cout<<"pthread_join end!"<<endl;
pthread_mutex_destroy(&g_lock);
return 0;
}
结果:
我们可以看到它只获取一张票就不再往下执行了,陷入了死锁中
这是因为有一个工作线程加锁之后没有进行解锁,其他线程再次去获取锁时,互斥锁中计数器中的值还是0,就要被阻塞等待,所以加锁之后一定要记得解锁
前面我们讲如果不进行解锁会造成死锁现象,但是死锁是什么?现在我们就来讲讲
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于一种永久等待状态。
就像下图
线程A获取到互斥锁1,线程B获取到互斥锁2时,线程A和线程B同时还想获取对方手里的锁(线程A还想获取互斥锁2,线程B还想获取互斥锁1),此时就会导致死锁
代码实现看一下
#include
#include
#include
using namespace std;
pthread_mutex_t lock1;
pthread_mutex_t lock2;
void* ThreadNum1(void* args)
{
(void*)args;
pthread_mutex_lock(&lock1);
sleep(3);
pthread_mutex_lock(&lock2);
return NULL;
}
void* ThreadNum2(void* args)
{
(void*)args;
pthread_mutex_lock(&lock2);
sleep(3);
pthread_mutex_lock(&lock1);
return NULL;
}
int main()
{
pthread_mutex_init(&lock1,NULL);
pthread_mutex_init(&lock2,NULL);
pthread_t tid;
int ret = pthread_create(&tid,NULL,ThreadNum1,NULL);
if(ret < 0)
{
cout<<"thread1 create failed"<<endl;
return 0;
}
ret = pthread_create(&tid,NULL,ThreadNum2,NULL);
if(ret < 0)
{
cout<<"thread2 create failed"<<endl;
return 0;
}
while(1)
{
;
}
pthread_mutex_destroy(&lock1);
pthread_mutex_destroy(&lock2);
return 0;
}
死锁的生成有四个必要的条件:
想要预防死锁只要破坏死锁4个条件中的一个即可
它和死锁预防的差别很小,可以把它理解为死锁预防的一种特例。
死锁避免策略在允许三个必要条件存在的条件下,来确保永远不会达到死锁点。
死锁避免方法有:
相比死锁预防策略,死锁避免策略并发性更强。但是在使用中也有诸多限制:
编号 | 比较项 | 预防死锁 | 避免死锁 |
---|---|---|---|
1 | 概念 | 预防死锁至少阻止了发生死锁的必要条件之一。 | 避免死锁确保系统不会进入不安全状态 |
2 | 资源请求 | 预防死锁所有的资源都是一起请求的。 | 资源请求是根据可用的安全路径完成的。 |
3 | 所需信息 | 预防死锁不需要关于现有资源、可用资源和资源请求的信息 | 避免死锁需要关于现有资源、可用资源和资源请求的信息 |
4 | 过程 | 通过限制资源请求过程和资源处理来防止死锁。 | 避免死锁会自动考虑请求并检查它是否对系统安全。 |
5 | 抢占 | 有时,抢占会更频繁地发生。 | 避免死锁在死锁避免中没有抢占。 |
6 | 资源分配策略 | 用于防止死锁的资源分配策略是保守的。 | 防止死锁的资源分配策略并不保守。 |
7 | 未来的资源请求 | 预防死锁不需要知道未来的进程资源请求。 | 避免死锁需要了解未来的进程资源请求。 |
8 | 优点 | 预防死锁不涉及任何成本,因为它只需使条件之一为假,这样就不会发生死锁。 | 由于此方法动态工作以分配资源,因此没有系统未充分利用。 |
9 | 缺点 | 死锁预防设备利用率低。 | 避免死锁会使进程阻塞太久。 |
10 | 使用示例 | 假脱机和非阻塞同步算法。 | 使用银行家和安全算法。 |