Linux 多线程 | 线程的互斥

在前面的文章中我们讲述了多线程的一些基本的概念以及相关的操作,那么在本章中我们就将继续讲述与多线程相关的同步与互斥之间的问题。

首先我们使用一个例子引出我们的问题,又一个全局的变量g_val = 100,这个变量是被所有的执行流所共享的,那么就可能会存在并发访问的问题。这个问题最可怕的就是当一个执行流在使用的时候,另一个执行流同样要进行操作。假设我们有线程A和线程B都要执行while(g_val--);的操作,当计算机要执行g_val--的操作的时候,从代码上来看只需要进行一步指令,但是从计算机的角度来说要先将数据从内存中加载到CPU中的寄存器中,在寄存器中进行--操作,然后再重新将数据放回内存中。按照下图进行基本的g_val--操作。

然后,开始程序的运行,首先是线程A,线程A的运气不是很好,当线程A计算刚刚进行第二步的时候,此时线程B要开始调度,那么经过之前学习的知识,需要将线程A的上下文进行保存。然后线程B就开始运行,线程B的运气比较好,一直运行将全局变量的值已经减到了10,当B线程想要再往后运行时遇到了和A 一样的问题,那么B也需要进行上下文的保存。当线程A的上下文重新被启用的时候,此时就会遇到一个问题,虽然我们已经计算将g_val的值减为10,但是由于A中上下文保存的数值是100,因此A开始运行的时候依旧是从100开始计算的。那么这里就出现了我们之前所述的问题:并发访问,进而导致数据不一致的问题。

  • 因此就需要我们对共享的资源进行保护 - 临界资源 - 衡量共享资源;
  • 我们的任何一个线程都有代码访问临界资源(临界区),同样的不访问临界资源的区域(非临界区) - 衡量线程代码。
  • 当我们想要让多个线程安全的访问临街资源,就可以使用加锁的方式进行互斥访问。
  • --操作并不是原子的,而对应了三条汇编指令:load :将共享变量ticket从内存加载到寄存器中;update : 更新寄存器里面的值,执行-1操作;store :将新值,从寄存器写回共享变量ticket的内存地址。为了让我们上述说的三条指令看起来像一条指令,那么就需要让这些指令是原子性的。

互斥量的接口

初始化互斥量

初始化互斥量有两种方法:静态分配,在全局范围定义 

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

动态分配,在局部定义需要初始化与销毁

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrictattr);
参数:mutex:要初始化的互斥量attr:NULL

销毁互斥量

销毁互斥量需要注意:使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁不要销毁一个已经加锁的互斥量已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥量加锁与解锁

互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

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

简单的demo

#include <iostream>
#include <cstdio>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstring>using namespace std;int tickets = 1000; // 临界资源, 加锁保证临界
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 使用这种方式不用进行destory,若是在局部那么就需要使用pthread_mutex_init来进行初始化
void* threadRoutine(void* name)
{string tname = static_cast<const char*>(name);while (true){pthread_mutex_lock(&mutex); // 所有的线程都需要遵守这个规则if (tickets > 0) // 临界区{usleep(2000); // 模拟抢票花费的时间,在此处进行线程的切换导致有多个进程同时处于此状态cout << tname << " get a ticket: " << tickets-- << endl;pthread_mutex_unlock(&mutex); }else{pthread_mutex_unlock(&mutex);break;}usleep(1000); // 抢完票的时候需要有后续的动作}return nullptr;
}int main()
{pthread_t tids[4];int n = sizeof(tids)/sizeof(tids[0]);for (int i = 0; i < n; ++i){char* data = new char[64];snprintf(data, 64, "thread-%d", i + 1);pthread_create(tids+i, nullptr, threadRoutine, data);}for (int i = 0; i < n; ++i){pthread_join(tids[i], nullptr);}return 0;
}

这里还有一些细节: 

1. 凡是访问同一个临界资源的线程,都要进行加锁保护,而且必须加同一把锁,这是一个游戏规则,不能有例外。

2. 每一个线程访问临界区之前,得加锁,所以加锁是给临界区加锁,加锁的粒度尽量要细一些。

3. 线程访问临界区的时候,需要先加锁->所有的线程都必须看到同一把锁->锁本身就是公共资源->锁如何保证自己的安全?-> 加锁和解锁本身就是原子的!

4. 临界区可以是一行代码,可以是一批代码, a. 线程可能被切换吗?可能,不要特殊化加锁与解锁,还有临界区代码 b. 切换会有影响吗?不会因为在我不在的期间,任何人无法进入临界区,应为它无法申请到锁,因为锁被我拿走了。

5. 这正是体现互斥带来的串行化的表现,站在其他人的角度对其他线程有意义的状态就是:锁被我申请(持有锁),锁被我释放了(不持有锁),原子性就体现在这里。

6. 解锁的过程也应该被设计为原子的。

互斥锁实现原理探究

下面我们来简要的介绍一下互斥锁的实现原理:互斥量。

经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们看一下lock和unlock的伪代码。

这里我们默认 mutex = 1;

lock:movb $0, %al // 调用线程,向自己的上下文中写入0xchgb %al, mutex if(al寄存器内容 > 0){ return 0;}else 挂起等待;goto lock;unlock:movb $1, mutex唤醒等待mutex的线程;return 0;

1. swap或exchange指令,该指令的作用是把寄存器和内存单元的数据进行相交换

2. 谁在执行加锁与解锁的代码?调用线程

3. 寄存器硬件只有一套,但是寄存器内部的数据是每一个线程都要有的 -->
寄存器 != 寄存器的内容(执行流的上下文)

其中第二句指令的交换:交换(一条汇编,体现加锁的原子性)的本质是: 将共享数据交换到自己的私有上下文当中 -- 加锁; 因为这里是交换所以不会有任何1的新增,那么由于交换是原子的,那么公共的mutex中的1就会被持有锁的那个线程的上下文占用,当其余持有锁的线程被加载到CPU的时候,由于mutex中的值是0, 因此该线程就会被挂起直到能够取到mutex中的1,即获取到锁。

解锁的时候就将1直接mov到mutex中即可。


 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/449255.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

通过 ChatGPT 的 Function Call 查询数据库

用 Function Calling 的方式实现手机流量包智能客服的例子。 def get_sql_completion(messages, model"gpt-3.5-turbo"):response client.chat.completions.create(modelmodel,messagesmessages,temperature0,tools[{ # 摘自 OpenAI 官方示例 https://github.com/…

Jetpack Compose系列(1)-初识Jetpck

Jetpack Compose是什么 2019年的I/O大会上&#xff0c;Google宣布Kotlin成为Android开发首选语言&#xff08;这次不是第一次说了&#xff09;&#xff0c;且后续会有新的Jetpack API和功能将在Kotlin中提供&#xff0c;并同时开源Jetpack Compose。 简介 Jetpack是一套库、…

题目: 有1234个数字, 组成多个互不相同且无重复数字的三位数? 都是多少?

lua脚本如下 最原始的解题方法 local str{} local i, j, k0, 0, 0 for i1, 4 do for j1, 4 do for k1, 4 do if i~j and i~k and j~k then str[#str1]i..j..k end end end end print("组成的数有"..#str) print(table.unpack(str)) 运行的结果如下 组成的数有24 1…

EF Core入门例子(以SqLite为数据库)

测试环境&#xff1a; visual studio 2017 .net core 2.1 具体步骤如下&#xff1a; 1 新增名称为EFCoreDemo的.net core控制台程序&#xff0c;版本选择.net core 2.1&#xff0c;项目不能放到带中文的目录下&#xff0c;不然到后面执行Add-Migration命令时会报如下的错误…

【android】对于google-webrtc的性能中, memory leak

目录 zlmediakit->webrtcplay->app webrtcutil1/3 测试程序等 zlmediakit->webrtcplay->app 编译sdk 32 有时候会从开始新增5M&#xff0c;就稳定在一个值了 webrtcutil1/3 测试程序等 编译sdk 30

基于springboot实现二次元商品购物系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现二次元商品购物系统演示 摘要 时代的变化速度实在超出人类的所料&#xff0c;21世纪&#xff0c;计算机已经发展到各行各业&#xff0c;各个地区&#xff0c;它的载体媒介-计算机&#xff0c;大众称之为的电脑&#xff0c;是一种特高速的科学仪器&#xff0…

LeetCode:138. 随机链表的复制之如何有效copy

自己复制的话&#xff0c;很容易写出来一个时间复杂度O&#xff08;n ^ 2&#xff09; 空O&#xff08;n&#xff09;的做法 我们可以参考基因的复制&#xff0c; 目录 题目&#xff1a; 实现思路&#xff08;基因复制式的copy&#xff09;&#xff1a; 官方快慢指针解法&…

呼叫中心座席转接策略

在企业进行批量呼出任务的时候&#xff0c;为了最大效率使用坐席&#xff0c;通常是以班组为单位&#xff0c;进行批量呼出任务。在选择班组作为呼叫业务的基本单位时&#xff0c;就涉及到为呼叫选择坐席策略。 OKCC系统的班组对于选择坐席设计了五种策略&#xff0c;即轮选、最…

TCP/IP详细介绍以及TCP/IP寻址

目录 ​编辑 1. TCP/IP 介绍 2. 计算机通信协议&#xff08;Computer Communication Protocol&#xff09; 3. 什么是 TCP/IP&#xff1f; 4. 在 TCP/IP 内部 5. TCP 使用固定的连接 6. IP 是无连接的 7. IP 路由器 8. TCP/IP 9. TCP/IP 寻址 10. IP地址 …

【Springcloud篇】学习笔记二(四至六章):Eureka、Zookeeper、Consul

第四章_Eureka服务注册与发现 1.Eureka基础知识 1.1Eureka工作流程-服务注册 1.2Eureka两大组件 2.单机Eureka构建步骤 IDEA生成EurekaServer端服务注册中心&#xff0c;类似于物业公司 EurekaClient端cloud-provider-payment8081将注册进EurekaServer成为服务提供者provide…

国家博物馆逆向抢票协议

逆向工程的具体步骤可以因项目和目标系统的不同而有所变化。然而&#xff0c;以下是一般逆向工程的一般步骤&#xff1a; 1. 分析目标系统&#xff1a;对待逆向的系统进行调研和了解&#xff0c;包括其架构、功能、使用的技术等方面的信息。 2. 反汇编或反编译&#xff1a;使…

代码随想录算法训练营第38天 | 动态规划理论基础 + 509.斐波那契数 + 70.爬楼梯 + 746.使用最小花费爬楼梯

今日任务 理论基础 509. 斐波那契数 70. 爬楼梯 746. 使用最小花费爬楼梯 动态规划理论基础 理论基础&#xff1a;代码随想录 动态规划&#xff0c;英文&#xff1a;Dynamic Programming&#xff0c;简称DP&#xff0c;如果某一问题有很多重叠子问题&#xff0c;使用动态规划…