【Linux】线程互斥

在之前的博客中,我讲到线程的相关概念和线程的控制,在本节中我们聊一下线程互斥。

五个概念
  • 临界资源
    多线程执行流共享的资源叫做临界资源。大量执行流同时访问时可能会导致数据二义性的问题。
  • 临界区
    每个线程内部访问临界资源区的代码叫做临界。我们可以通过控制代码的读写规则保证临界区的安全性。
  • 互斥
    任何时刻有且仅有一个执行流进入临界区的情况称互斥。我们通常在访问临界资源时会对它进行加锁保护,保证数据的安全性。
  • 同步
    在保证临界资源安全的情况下让多执行流按一定顺序访问它称同步。同步非同时,保证同步是为了协调多执行流的步调,避免饥饿问题。
  • 原子性
    不会被任何调度机制打断的操作,该操作只有两种状态,要么完成,要么未开始。一般只需要一条汇编代码就可以完成解析。
多线程并发的问题

大部分情况下,线程使用的数据局部变量,变量的地址空间在线程栈空间内,这种情况下变量只属于单个线程,其他线程无法访问。但是也有时候,我们的变量需要在进程间共享,这样的变量称为共享数据可用于完成线程间的交互。

然而多线程并发的操作共享变量会带来一些问题:
先看下面代码

#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;
int goal=0;
void* handle(void* arg)
{
	int count=10000000;
	while(count--)
		goal++;
	return (void*)1;
}
int main()
{
	pthread_t t1,t2;
	pthread_create(&t1,NULL,handle,NULL);
	pthread_create(&t2,NULL,handle,NULL);
	sleep(1);
	cout<<goal<<endl;
	pthread_join(t1,NULL);
	pthread_join(t2,NULL);
	return 0;
}

不看不知道,一看吓一跳!!本来我以为我写了两个线程分别对goal加10000000次,最后的结果应该是20000000,然而!!
在这里插入图片描述
通过运行结果,我发现运行了三次的结果都不相同,而且也没有正确的结果。那么问题出在哪里呢?
原来是因为两个线程是并发运行的,不同的线程在执行时有不同的寄存器,他们可能同时取走了某一时刻的goal,对它分别进行++后结果都加了一,但最后写回内存时由原来的加2变成只加1,导致结果越错越离谱。这里的根本原因是++这个操作并不是原子性的。看下图你应该就能明白了
在这里插入图片描述
来看我们在vs2013下一段简单的反汇编代码:从图中你可以清楚的看到对于goal的++操作分解成了三步,所以这就是导致上面问题的罪魁祸首
在这里插入图片描述
如果++是一个原子性的操作的话,那么就不会出现上述的问题,所以现在要想解决上面的问题要么将上述代码实现原子性操作,要么实现互斥的机制,Linux中确实提供了原子的++操作(atomic)但是这仅仅解决了当前问题,所以我们这里重点介绍线程的互斥锁实现互斥。

互斥量mutex

为了解决上面的问题,我们需要做到3点:

  1. 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区
  2. 如果多个线程同时要执行临界区的代码,并且没有线程在执行,那么只允许一个线程进入该临界区
  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

要做到以上三点,本质上需要一把锁,Linux下提供了一把锁叫做互斥量:
在这里插入图片描述
接下来我介绍一下互斥量的接口。

互斥量的接口

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

  1. 静态分配
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  2. 动态分配
    int pthread_mutex_init(pthread_mutex_t restrict mutex, const pthread_mutexattr_trestrict attr);
    //参数1为要初始化的互斥锁,参数2一般设置为NULL表示使用默认属性

销毁互斥量:

int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意:使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁
不要销毁一个被加锁的互斥量
已经销毁的互斥量不能再被线程加锁

互斥量加锁和解锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
注意:调用pthread_lock时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁

下面我们使用上面的函数,修改之前的代码为线程安全的:

在这里插入图片描述
这时如果当前没有线程在临界区中,那么线程一访问临界资源的时候会先进行加锁处理,当线程二要访问临界资源时发现资源被锁,所以OS将线程二设置为非r状态并将其加入到阻塞队列中等待线程一释放锁资源。这样就可以保证线程在对临界区访问时任一时刻只有一个线程,也就是说我们现在可以认为我们的程序++是原子的。
运行结果如下:
在这里插入图片描述

互斥量实现的原理

互斥锁的实现原理实际上非常的简单,我之前说到过,当汇编是一句指令时就可以认为是原子的,所以为了实现锁的互斥操作,大多数体系结构对于互斥锁的实现使用了swap或者exchange指令,该指令的作用是把寄存器和单元数据交换,因为就算是多线程,总线周期也有先后时间,所以利用此指令总能保证同一个时间只有一个线程进入临界区。

现在我们来看看如何使用swap或者exchange指令实现互斥锁原理,来看下面的一段伪代码:可以看到锁资源最开始拿到的值为1,然后使用swap或exchange指令将寄存器中的值和锁拥有的值交换,如果寄存器中现在值为1,那么表示资源申请成功,如果寄存器中值为0,说明锁资源中的1已经被其他线程交换走了,所以当前进程也就需要进入等待队列。
在这里插入图片描述
注意: 加锁操作要求原子性,解锁操作无要求。

线程安全

线程安全:多个线程并发处理同一段代码时,不会出现不同的结果。常见的对全局变量或者静态变量进行操作并且没有锁的保护下,会出现不同的结果。
重入:同一函数被不同的执行流调用,当前执行流未执行完毕就有其他执行流再次进入,称这种现象为重入。一个函数在重入情况下,运行结果不会出现任何问题,称为可重入函数,否则称为不可重入函数。

常见线程不安全的情况:
  1. 不保护共享变量的函数
  2. 函数的状态随着被调用,函数发生变化的函数
  3. 返回指向静态变量指针的函数
  4. 调用线程不安全函数的函数
常见线程安全的情况:
  1. 每个线程对于全局变量或者静态变量只有读取的权限,而没有写入的权限
  2. 类或者接口对于线程来说都是原子操作
  3. 多个线程之间的切换不会导致该接口的执行结果存在二义性
可重入与线程安全
  • 可重入函数一定是线程安全的,线程安全的函数不一定可重入
  • 不可重入函数不能由多个线程使用过,否则有线程安全问题
  • 若一个函数中有全局变量,则这个函数既不是线程安全也不是可重入的
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值