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 // 调用线程,向自己的上下文中写入0
    xchgb %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中即可。


 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL进阶45讲【10】MySQL为什么有时候会选错索引?

1 前言 前面我们介绍过索引&#xff0c;在MySQL中一张表其实是可以支持多个索引的。但是&#xff0c;写SQL语句的时候&#xff0c;并没有主动指定使用哪个索引。也就是说&#xff0c;使用哪个索引是由MySQL来确定的。 大家有没有碰到过这种情况&#xff0c;一条本来可以执行得…

VSCode snippets 自定义Vue3代码片段(持续更新)

在编写Vue代码时发现VSCode中的各类snippets插件无法提供一些常用的代码片段,为避免重复造轮子,提高编码效率,特意自己定义了一些代码片段。为方便初学者,提供了自定义代码片断的方法。 一、 自定义代码片断的方法 1.打开命令面板(Ctrl+Shift+P) 2. 输入 user Snippets…

Hadoop3.x基础(3)- Yarn

来源&#xff1a;B站尚硅谷 目录 Yarn资源调度器Yarn基础架构Yarn工作机制作业提交全过程Yarn调度器和调度算法先进先出调度器&#xff08;FIFO&#xff09;容量调度器&#xff08;Capacity Scheduler&#xff09;公平调度器&#xff08;Fair Scheduler&#xff09; Yarn常用命…

C语言-2

自定义类型 基本认识 /*引入&#xff1a;学生&#xff1a;姓名&#xff0c;学号&#xff0c;年龄&#xff0c;成绩请为学生们专门定制一个类型&#xff08;创造一个类型&#xff09;结构体格式&#xff1a;struct 标识符 // 标识符即自定义类型的名称{成员; // 自己设置…

【Unity知识点详解】自定义程序集

今天来介绍一下Unity中的自定义程序集。在项目开发中我们经常接触到第三方插件的程序集&#xff0c;如DOTween、Newtonsoft.Json等。 使用自定义程序集有这么几个好处&#xff1a; 方便代码的的复用。当某一功能模块需要在多个项目中重复使用时&#xff0c;可以将代码编译成程…

新书速览|Kubernetes从入门到DevOps企业应用实战

从0到1&#xff0c;从零开始全面精通Kubernetes&#xff0c;助力企业DevOps应用实践 本书内容 《Kubernetes从入门到DevOps企业应用实战》以实战为主&#xff0c;内容涵盖容器技术、Kubernetes核心资源以及基于Kubernetes的企业级实践。从容器基础知识开始&#xff0c;由浅入深…

C#中使用OpenCvSharp4绘制直线、矩形、圆、文本

C#中使用OpenCvSharp4绘制直线、矩形、圆、文本 继之前的Python中使用Opencv-python库绘制直线、矩形、圆、文本和VC中使用OpenCV绘制直线、矩形、圆和文字&#xff0c;将之前的Python和C示例代码翻译成C#语言&#xff0c;很简单&#xff0c;还是借用OpenCvSharp4库中的Line、…

基于腾讯云服务器搭建幻兽帕鲁服务器保姆级教程

随着网络游戏的普及&#xff0c;越来越多的玩家希望能够拥有自己的游戏服务器&#xff0c;以便能够自由地玩耍。而腾讯云服务器作为一个优秀的云计算平台&#xff0c;为玩家们提供了一个便捷、稳定、安全的游戏服务器解决方案。本文将为大家介绍如何基于腾讯云服务器搭建幻兽帕…

Fink CDC数据同步(三)Flink集成Hive

1 目的 持久化元数据 Flink利用Hive的MetaStore作为持久化的Catalog&#xff0c;我们可通过HiveCatalog将不同会话中的 Flink元数据存储到Hive Metastore 中。 利用 Flink 来读写 Hive 的表 Flink打通了与Hive的集成&#xff0c;如同使用SparkSQL或者Impala操作Hive中的数据…

从MySQL到TiDB:兼容性全解析

MySQL 在高并发和大数据量场景下&#xff0c;单个实例的扩展性有限。而 TiDB 作为一款分布式NewSQL数据库&#xff0c;设计之初就支持水平扩展&#xff08;Scale-Out&#xff09;&#xff0c;通过增加节点来线性提升处理能力和存储容量&#xff0c;能够很好地应对大规模数据和高…

AS-V1000 视频监控平台产品介绍:客户端功能介绍(一)

目 录 一、引言 1.1 AS-V1000视频监控平台介绍 1.2平台服务器配置说明 二、软件概述 2.1 客户端软件用途 2.2 客户端功能 三、客户端功能说明 3.1 登陆和主界面 3.1.1登陆界面 3.1.2登陆操作 3.1.3主界面 3.1.4资源树 3.2 视频预览 3.2.1视频预览界面 3.2.…

python 基础知识点(蓝桥杯python科目个人复习计划33)

今日复习内容&#xff1a;以做题为主 例题1&#xff1a;小蓝的漆房 题目描述&#xff1a; 小蓝是一位有名的漆匠&#xff0c;他的朋友小桥有一个漆房&#xff0c;里面有一条长长的走廊&#xff0c;走廊两旁有许多相邻的房子&#xff0c;每间房子最初被涂上了一种颜色。 小桥…

Lambda表达式(匿名函数)

C11中引入了lambda表达式&#xff0c;定义匿名的内联函数。 我们可以直接原地定义函数而不用再跑到外面去定义函数跳来跳去。 同时在stl的排序上也有作用。 [capture] (parameters) mutable ->return-type {statement}下面逐一介绍各个参数的含义. [capture] : 捕获&#…

喜讯!亚信安慧斩获第六届金猿奖两大奖项!

近日&#xff0c;第六届金猿奖颁奖典礼在上海 “第六届金猿季&魔方论坛——大数据产业发展论坛”上隆重举行&#xff0c;湖南亚信安慧科技有限公司&#xff08;简称“亚信安慧”&#xff09;凭借AntDB数据库获评“2023大数据产业年度创新技术突破” 、“2023大数据产业年度…

解锁MyBatis Plus的强大功能:学习高级操作与DML技巧!

MyBatisPlus 1&#xff0c;DML编程控制1.1 id生成策略控制知识点1&#xff1a;TableId1.1.1 环境构建1.1.2 代码演示AUTO策略步骤1:设置生成策略为AUTO步骤3:运行新增方法 INPUT策略步骤1:设置生成策略为INPUT步骤2:添加数据手动设置ID步骤3:运行新增方法 ASSIGN_ID策略步骤1:设…

【数据结构 09】哈希

哈希算法&#xff1a;哈希也叫散列、映射&#xff0c;将任意长度的输入通过散列运算转化为固定长度的输出&#xff0c;该输出就是哈希值&#xff08;散列值&#xff09;。 哈希映射是一种压缩映射&#xff0c;通常情况下&#xff0c;散列值的空间远小于输入值的空间。 哈希运…

thinkadmin的form.html表单例子

<style>textarea {width: 100%;height: 200px;padding: 10px;border: 1px solid #ccc

JUC并发工具类的应用场景详解

目录 常用并发同步工具类的真实应用场景 1. ReentrantLock 1.1 常用API 1.2 ReentrantLock使用 独占锁&#xff1a;模拟抢票场景 公平锁和非公平锁 可重入锁 结合Condition实现生产者消费者模式 1.3 应用场景总结 2. Semaphore 2.1 常用API 2.2 Semaphore使…

VMware无法检测到插入的USB设备,虚拟机插拔USB无反应

原本正常使用的VMware虚拟机&#xff0c;在进行了重装软件后&#xff0c;发现虚拟机插拔USB设备都无法检测到&#xff0c;没有任何的反应和提示。 通过一系列的操作发现&#xff0c;在新安装了VMware workstation 软件后&#xff0c;存在一定的概率性会发生VMware虚拟机无法自…

在windows下安装docker部署环境运行项目----docker-compose.yml

前言 小编我将用CSDN记录软件开发求学之路上亲身所得与所学的心得与知识&#xff0c;有兴趣的小伙伴可以关注一下&#xff01; 也许一个人独行&#xff0c;可以走的很快&#xff0c;但是一群人结伴而行&#xff0c;才能走的更远&#xff01;让我们在成长的道路上互相学习&…