当前位置: 首页 > news >正文

Linux:线程互斥

线程互斥

先看到一个抢票案例:

class customer
{
public:int _ticket_num = 0;pthread_t _tid;string _name;
};int g_ticket = 10000;void* buyTicket(void* args)
{customer* cust = (customer*)args;while(true){if(g_ticket > 0){usleep(1000);cout << cust->_name << " get ticket: " << g_ticket << endl;g_ticket--;cust->_ticket_num++;}else{break;}}return nullptr;
}int main()
{vector<customer> custs(5);for(int i = 0; i < 5; i++){custs[i]._name= "customer-" + to_string(i + 1);pthread_create(&custs[i]._tid, nullptr, buyTicket, &custs[i]);}for(int i = 0; i < 5; i++){pthread_join(custs[i]._tid, nullptr);}for(int i = 0; i < 5; i++){cout << custs[i]._name << " get tickets: " << custs[i]._ticket_num << endl;}return 0;
}

输出结果: 

g_ticket--本质上是多条汇编语句,比如下面这样:

MOV  eax, [0x1000]  ; 读取 g_ticket 的值
DEC  eax            ; 减 1
MOV  [0x1000], eax  ; 将值写回 g_ticket

为什么最开始的案例中,会出现10005张票,就是因为最后一张票,被五个线程同时抢到了!当g_ticket已经被抢走时,由于没来得及g_ticket = 0,导致后来的线程以为还有票。

引入一部分概念,方便理解后续知识:

  • 临界资源:以上案例中,g_ticket是共享资源,多个线程共享。我们把这种资源称为临界资源
  • 临界区:访问临界资源的代码,叫做临界区。比如g_ticket--就是临界区代码,以为其访问了临界资源g_ticket
  • 原子性:表示一个操作对外表现只有两种状态:还没开始已经结束

我们说g_ticket--本质上会变成多条汇编语句,也就是说g_ticket--是有过程的,而不是一瞬间完成的。

这就导致在我还没有完成g_ticket--的时候,也就是在g_ticket--过程中,被其他线程打断了。导致其它线程收到错误的信息,抢到不存在的票。

如果说一个线程抢到票后,g_ticket--会立马执行完毕,下一个线程在访问这个g_ticket > 0的时候,一定是在别人已经g_ticket--完毕,而不是在g_ticket--过程中,就可以避免这个问题。这就要求访问g_ticket是原子性的,也就是说在别的线程眼中,根本就不存在g_ticket--的过程,要么你没有执行g_ticket--,要么已经执行完毕。

临界区代码只要保证是原子性的,就可以避免这样线程之间错误的抢占资源相同资源的问题

 那么要如何保证临界区代码是原子性的呢?此时就需要线程互斥了!

线程互斥指的是在多线程环境中,多个线程访问同一个共享资源时,只允许一个线程访问,其他线程必须等待,直到当前线程访问完成才能继续访问。

线程互斥,是通过来实现的。

锁的规则如下:

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

简单来说就是:任何时候临界区都只能有一个线程执行!


互斥锁 mutex

互斥锁pthread库提供的,英文名为mutex(互斥),需要头文件<pthread.h>

互斥锁的类型是pthread_mutex_t,分为全局互斥锁局部互斥锁,它们的创建方式不同。

全局mutex

想要创建一个全局的互斥锁很简单,直接定义即可:

pthread_mutex_t xxx = PTHREAD_MUTEX_INITIALIZER;

这样就创建了一个名为xxx的变量,类型是pthread_mutex_t,即这个变量是一个互斥锁全局的互斥锁必须用宏PTHREAD_MUTEX_INITIALIZER进行初始化! 

另外,全局的互斥锁不需要手动销毁。

局部mutex

局部的互斥锁是需要通过接口来初始化与销毁的,接口如下:

pthread_mutex_init

pthread_mutex_init函数用于初始化一个互斥锁,函数原型如下:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *res

参数:

  • restrict mutex:类型为pthread_mutex_t *的指针,指向一个互斥锁变量,对其初始化
  • restrict attr:用于设定该互斥锁的属性,一般不用,设为空指针即可

返回值:成功返回0;失败返回错误码


pthread_mutex_destroy

pthread_mutex_destroy函数用于销毁一个互斥锁,函数原型如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数:类型为pthread_mutex_t *的指针,指向一个互斥锁变量,销毁该锁

返回值:成功返回0;失败返回错误码


创建好互斥锁后,就要使用这个锁,主要是两个操作:申请锁释放锁

三个函数的原型

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • pthread_mutex_lock:用于申请锁,如果申请失败,就阻塞等待,直到申请到锁;如果申请成功,就执行临界区代码
  • pthread_mutex_trylock:用于申请锁,如果申请失败,直接返回,而不是等待;如果申请成功,就执行临界区代码
  • pthread_mutex_unlock:用于释放锁,表明自己已经访问完毕临界区,其他线程可以来访问了

修改一下最初的抢票代码,给它加锁,保证抢票g_ticket--的原子性:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //全局互斥锁void *buyTicket(void *args)
{customer *cust = (customer *)args;while (true){pthread_mutex_lock(&mutex); // 加锁if (g_ticket > 0){usleep(1000);cout << cust->_name << " get ticket: " << g_ticket << endl;g_ticket--;pthread_mutex_unlock(&mutex); // 解锁cust->_ticket_num++;}else{pthread_mutex_unlock(&mutex); // 解锁break;}}return nullptr;
}

 


互斥锁原理

那么互斥锁是如何做到的呢?

互斥锁的汇编伪代码如下:

加锁lock

moveb $0, %al
xchgb %al, mutex
if (al寄存器的内容 > 0){return 0;
}else挂起等待;
goto lock

         

如图所示,现在有两个线程thread-1和thread-2,它们共同征用内存中的锁mutex。在CPU中有一个寄存器%al,用于存储和锁的值。

现在假设thread-1进行调度执行pthread_mutex_lock:

首先执行指令moveb $0, %al,你可以理解为,就是把%al寄存器内部的值变成0:

随后执行xchgb %al, mutex,该过程是让内存中的mutex%al寄存器的值进行交换: 

此时%al寄存器的值变成1mutex的值变成0。随后执行:

if (al寄存器的内容 > 0){return 0;
}else挂起等待;

也就是说判断当前%al内部的值是0还是大于0,如果大于0那么说明争夺到了锁,此时函数pthread_mutex_lock返回0,表示加锁成功。否则执行else进行挂起等待。

这样一个线程就征用到了一把锁。

现在假设thread-1执行到第一条汇编语句后,%al的值还是0thread-2调度了:

现在thread-1保存自己的硬件上下文,包括%al = 0在内,随后therad-2进入: 现在thread-2执行了两行汇编语句,成功把内存中的mutex与自己的%al交换,申请到了锁,此时thread-1再次调度,thread-2拷贝走自己的硬件上下文:

恢复硬件上下文后,thread-1%al等于0,执行第二条语句后,%almutex依然是0,这表明锁已经别的线程拿走了,此时在执行if内部的内容,thread-1挂起等待。

可以看到,其实锁的本质,就是保证mutex变量中以及所有访问锁的线程的%al寄存器中,只会有一个非零值。只有拿到非零值的线程才有资格去访问临界资源。其它线程如果要再次申请锁,由于自己的%almutex都是0,就算交换后还是0,也申请不到锁。 

并不是谁先调用ptherad_mutex_lock,谁就先抢到锁,而是谁先执行该函数内部的xchgb %al, mutex语句,把非零值放到自己的%al中,谁才抢到锁。

unlock

moveb $1, mutex
唤醒等待mutex的线程;
return 0;

解锁就很简单了,moveb $1, mutex就是把自己的%al中的1还给mutex,然后唤醒所有等待该锁的线程,让它们再次争夺这把锁。最后return 0,也就是pthread_mutex_unlock函数返回0


常见的锁

Linux中不仅仅存在互斥锁这一种锁,还有非常多的锁,接下来我们看看其它的锁。

死锁

死锁:指在一组进程中的各个进程均占有不会释放的资源,但因互相申请其它进程不会释放的资源而处于的一种永久等待状态

 现在有两个线程thread-1thread-2,以及两把互斥锁mutex-1mutex-2

现在要求:一个线程想要访问临界资源,必须同时持有mutex-1mutex-2。随后therad-1去申请了mutex-1thread-2去申请了mutex-2 thread-1再去申请mutex-2,结果mutex-2已经被therad-2占用了,thread-1陷入阻塞:

thread-2再去申请mutex-1,结果mutex-1已经被therad-1占用了,thread-2陷入阻塞:         

想要造成死锁,有四个必要条件:

  1. 互斥条件:一个资源每次只能被一个执行流使用
  2. 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:一个执行流获得资源后,其它执行流不能强行剥夺
  4. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

以上是比较正式的说法,接下来我从线程角度简单翻译翻译:

  1. 互斥条件临界资源同时只能被一个线程访问=
  2. 请求与保持条件:请求是指:申请对方的锁,保持是指:占着自己有的锁不放
  3. 不剥夺条件:一个线程如果申请锁失败,强行抢走他人的锁
  4. 循环等待条件:以刚刚的死锁为例,therad-1thread-2,而thread-2等待thread-1,形成一个头尾相接的循环

这四个条件都是必要条件,也就是说:

解决死锁,本质就是破坏一个或多个必要条件

主要有以下方式避免死锁:

  1. 破坏互斥条件:不要用锁
  2. 破坏请求与保持条件:如果发现没有申请到锁,立刻释放自己的全部锁
  3. 破坏不剥夺条件:如果发现没有申请到锁,强行释放对方的锁,将其占为己有
  4. 破坏循环等待条件:如果申请多把锁,所有线程都必须按照相同的顺序申请(最简单的方式)

自旋锁 spinlock

我们先前讲的锁,其机制是这样的:

当线程申请一个锁失败,就会阻塞等待,当锁被使用完毕,唤醒所有等待该锁的线程。

其实锁还有一种不用阻塞等待的策略,而是反复检测的策略,就像这样:

 

当线程没有申请到锁,一段时间后再次检测这个锁有没有被释放,一直反复申请这个锁,这个过程叫做自旋。基于这个策略来申请的锁,叫做自旋锁

Linux自带了自旋锁spinlock,类型为pthread_spinlock_t,接口如下:

创建与销毁

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);

加锁与解锁

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

你会发现,这和mutex几乎一摸一样,所以接口也就不讲解了。

不过我这里要强调一点,pthread_spin_lock并不是申请失败就返回,而是在pthread_spin_lock内部以自旋的方式申请锁,我们无需手动模拟自旋的过程。

 

 

 

 

 

 

 

        

 

 

 

 

 

 

 

 

 

 

http://www.lryc.cn/news/432975.html

相关文章:

  • misc流量分析
  • Linux驱动(五):Linux2.6驱动编写之设备树
  • 算法【Java】 —— 前缀和
  • python网络爬虫(四)——实战练习
  • tio websocket 客户端 java 代码 工具类
  • 通过卷积神经网络(CNN)识别和预测手写数字
  • 【A题第二套完整论文已出】2024数模国赛A题第二套完整论文+可运行代码参考(无偿分享)
  • 一份热乎的数据分析(数仓)面试题 | 每天一点点,收获不止一点
  • 3 html5之css新选择器和属性
  • 【Kubernetes】K8s 的鉴权管理(一):基于角色的访问控制(RBAC 鉴权)
  • 保研 比赛 利器: 用AI比赛助手降维打击数学建模
  • 秋招校招,在线性格测评应该如何应对
  • chrome 插件开发入门
  • 揭开面纱--机器学习
  • Python中的私有属性与方法:解锁面向对象编程的秘密
  • 开篇_____何谓安卓机型“工程固件” 与其他固件的区别 作用
  • DBeaver 连接 MySQL 报错 Public Key Retrieval is not allowed
  • 三个月涨粉两万,只因为知道了这个AI神器
  • vulhub GhostScript 沙箱绕过(CVE-2018-16509)
  • 李宏毅机器学习笔记——反向传播算法
  • 内推|京东|后端开发|运维|算法...|北京 更多岗位扫内推码了解,直接投递,跟踪进度
  • 编写Dockerfile第二版
  • 校验码:奇偶校验,CRC循环冗余校验,海明校验码
  • 增维思考,减维问题,避免焦虑!
  • 自动化抢票 12306
  • 海外云服务器安装 MariaDB10.6.X (Ubuntu 18.04 记录篇二)
  • Mybatis_基础
  • 8Manage采购申请管理:轻松实现手动采购流程自动化
  • PADS Router 入门基础教程(一)
  • 一台手机一个ip地址吗?手机ip地址泄露了怎么办