Linux线程同步与互斥(上)

目录

前言

1.互斥

1.先来见一种现象(数据不一致问题)

2.如何解决上述问题

3.理解为什么数据会不一致&&认识加锁的接口

4.理解锁

5.锁的封装


前言

  在前面对线程的概念和控制的学习过程中,我们知道了线程是共享地址空间的,也就是会共享大部分资源,那么这个时候就会产生新的问题——并发访问,最直观的感受就是每次运行得出的结果值大概率不一致,这种执行结果不一致的现象是非常致命,因为它具有随机性,即结果可能是对的,也可能是错的,无法可靠的完成任务

image-20250615172706982

  为了解决这一问题,我们要引入新的解决方案——同步和互斥,我们先来讲互斥!

1.互斥

image-20250615172818085

1.先来见一种现象(数据不一致问题)

• ⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程⽆法获得这种变量。

• 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

• 多个线程并发的操作共享变量,会带来⼀些问题,比如说下面的一段模拟抢票的实验代码

// 操作共享变量会有问题的售票系统代码
#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
​
int ticket = 100;
​
void *route(void *arg)
{char *id = (char *)arg;while (1){if (ticket > 0) // 1.判断{usleep(1000);                               // 模拟抢票化的时间printf("%s sells ticket:%d\n", id, ticket); // 2.模拟抢到了票ticket--;                                   // 3.票数--}else{break;}}return nullptr;
}
​
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");
​pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}

image-20250615183136247

可以看到结果都把票干到负数了,这在现实中可是一件很糟糕的事情,比如说高铁明明只有200个座位,却有201的人抢到了票,这个人是没有位置的,说明多个线程并发的操作共享变量,会带来⼀些问题

2.如何解决上述问题

上面的代码中

临界区:

while (1){if (ticket > 0) // 1.判断{usleep(1000);                               // 模拟抢票化的时间printf("%s sells ticket:%d\n", id, ticket); // 2.模拟抢到了票ticket--;                                   // 3.票数--}else{break;}}

共享资源是:int ticket =1000;

其他代码都属于非临界区

我们要想办法保护临界区:通过在临界区中前后加锁可以保护起来!

#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
​
int ticket = 100;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 对锁进行初始化
​
void *route(void *arg)
{char *id = (char *)arg;while (1){pthread_mutex_lock(&lock);if (ticket > 0) // 1.判断{usleep(1000);                               // 模拟抢票化的时间printf("%s sells ticket:%d\n", id, ticket); // 2.模拟抢到了票ticket--;                                   // 3.票数--pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break;}}return nullptr;
}
​
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");
​pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}

image-20250615184551620

可以从结果看到,此时就不会出现票数为负的情况了,顺利解决数据不一致的问题

3.理解为什么数据会不一致&&认识加锁的接口

首先我们需要知道的是ticket--不是原子性的操作,它会被汇编代码转换成三条指令

• load :将共享变量ticket从内存加载到寄存器中

• update : 更新寄存器⾥⾯的值,执⾏-1操作

• store :将新值,从寄存器写回共享变量ticket的内存地址

比如:

0xFF00 载入 ebx ticket
0xFF02 减少 ebx 1
0xFF04 写回 0x1111 ebx

假设我们有A、B两线程,ticket初始是100,在cpu调度A线程执行到0xFF04时要发生线程切换,此时需要保存A的上下文数据:ebx(ticket)为99,cpu的pc指针保存0xFF04地址,然后cpu开始调度B线程,B线程运气很好,在循环执行让ticket减到1之后刚好才要被切换,保存上下文之后cpu又重新调度A,此时pc指针保存的0xFF04地址是要执行写回内存的指令,那么这个时候的ticket又回到了99,这就发生了数据不一致问题,也说明了ticket--不是原子性的操作

image-20250615212831730

[^]  我们暂时这么去理解原子性:一条汇编就是原子的 

我们上面的票数减到负数其实主要的问题不是出在ticket--这个操作,而是出战if条件判断ticket>0这一操作上,对于ticket值是否大于0做判断也是一种计算(逻辑计算,得到的是布尔值),执行时先载入cpu,再判断;那么此时如果有3个线程,ticket此时为1,都完成1的载入后被切走了(因为加了休眠的时间,导致线程没来及做--操作就让下一个线程进来了),后面按顺序唤醒线程时时并行判断都是1就允许进入了,三个线程此时串行载入ticket,执行ticket--然后再写回内存使得ticket此时从1->0->-1->-2就变成-2了

上面的问题告诉了我们:全局资源没有加保护,可能会有并发问题——线程安全问题,同时要形成上面的问题需要在多线程中,制造更多的并发、更多的切换,切换的时间点:1.时间片到了 2.阻塞式IO 3.sleep等等...;选择新的线程时间点:从内核态返回用户态的时候,进行检查

要解决以上问题,需要做到三点:

• 代码必须要有互斥⾏为:当代码进⼊临界区执⾏时,不允许其他线程进⼊该临界区。

• 如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程进⼊该临界区。

• 如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区

要做到这三点,本质上就是需要⼀把锁 ——pthread_mutex_t(互斥锁/互斥量)

屏幕截图 2025-06-16 165944

[^]  pthread_mutex_init的第二个参数为锁属性,我们不用管设为nullptr就行 

加锁规则:尽量加锁的范围粒度要比较细,尽可能不要包含太多的非临界区代码

image-20250616161349990

对临界区进行保护本质其实就是用锁来对临界区进行保护

问题1:如果有线程不遵守我们的规则,那就是一个bug,所有线程必须遵守!!

问题2:枷锁之后,在临界区内部允许线程切换吗?切换了会怎么样?

答:允许切换,但是不会怎么样,因为我当前线程并没有释放锁,该线程持有锁被切换,

其他线程也必须等我被切换回来执行完代码、释放锁了才能展开申请锁的竞争,进而

进入临界区(当然这样就会导致多线程执行代码的速度变慢)

image-20250616165319481

加锁和解锁的本质就是把整个代码块进行原子化,让其他无法中断该线程

4.理解锁

经过上⾯的例⼦,⼤家已经意识到单纯的 i++或者 ++i都不是原⼦的,有可能会有数据⼀致性问题

锁的原理:

  1. 硬件级实现:关闭时钟中断

  2. 软件级实现:

    为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令(只有一条指令保证原子性),该指令的作用是把寄存器和内存单元的数据相交换

    下面是一段锁在汇编的伪代码:

    image-20250616193123421

image-20250616191606705

5.锁的封装

其实在c++中用锁很简单,我们只需要包含#include<mutex.h>头文件,然后定义一个锁被封装好的mutex类的对象,然后就可以用这个对象调用这个mutex类中的lock、unlock接口实现申请锁和解锁等操作啦(我们其实在c++阶段是学过的)

image-20250616195740905

使用c++封装的锁来解决我们上面的抢票数据不一致问题代码:

#include <iostream>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <mutex>
​
int ticket = 100;
// pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 对锁进行初始化
std::mutex lock;
​
void *route(void *arg)
{char *id = (char *)arg;while (1){// pthread_mutex_lock(&lock);lock.lock();if (ticket > 0) // 1.判断{usleep(1000);                               // 模拟抢票化的时间printf("%s sells ticket:%d\n", id, ticket); // 2.模拟抢到了票ticket--;                                   // 3.票数--// pthread_mutex_unlock(&lock);lock.unlock();}else{// pthread_mutex_unlock(&lock);lock.unlock();break;}}return nullptr;
}
​
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");
​pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}

我们当然也可以自己造个轮子,也跟着封装一个我们自己的锁

Mutex.hpp

#pragma once
#include <pthread.h>
#include <iostream>
​
namespace MutexModle
{class Mutex{public:Mutex(){pthread_mutex_init(&_mutex, nullptr);}
​// 申请锁void Lock(){// pthread_mutex_lock成功返回0,失败返回错误码int n = pthread_mutex_lock(&_mutex);if (n != 0){std::cerr << "申请锁失败" << std::endl;return;}}
​// 解锁void Unlock(){int n = pthread_mutex_unlock(&_mutex);if (n != 0){std::cerr << "解锁失败" << std::endl;return;}}
​~Mutex(){pthread_mutex_destroy(&_mutex);}
​private:pthread_mutex_t _mutex;};
​// 实现RAII风格的互斥锁class LockGuard{public:LockGuard(Mutex &mutex): _mutex(mutex){_mutex.Lock();}
​~LockGuard(){_mutex.Unlock();}
​private:Mutex &_mutex;};
}

TestMutex.cc

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "Mutex.hpp"
using namespace MutexModle;
​
int ticket = 100;
​
// 我们自己封装的锁类
Mutex lock;
​
void *route(void *arg)
{char *id = (char *)arg;while (1){// 申请锁// lock.Lock();// 通过LockGuard类构造对象调用构造函数中的申请锁代码实现自动加锁// 这就是RAII风格的互斥锁的实现LockGuard guard(lock);
​if (ticket > 0) // 1.判断{usleep(1000);                               // 模拟抢票化的时间printf("%s sells ticket:%d\n", id, ticket); // 2.模拟抢到了票ticket--;                                   // 3.票数--// 解锁// lock.Unlock();// 通过guard临时对象出作用域会自动调用析构函数进行自动解锁}else{// lock.Unlock();// 通过guard临时对象出作用域会自动调用析构函数进行自动解锁break;}}return nullptr;
}
​
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");
​pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
​return 0;
}

结果当然也是显而易见的成功解决数据不一致问题啦!

image-20250616204109527

我们上面其实实现了RAII风格(智能指针就是利用这个思想的)的互斥锁

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

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

相关文章

Codeforces Global Round 27

ABC 略D将每个数拆成x*2的整数次幂&#xff0c;一个直接的想法是尽量把2的整数次幂给大的数。那么所有乘上2的整数次幂的数构成的序列单调递减&#xff0c;反证法&#xff0c;如果序列中存在i j 使得a[i]<a[j]&#xff0c;那么我们不如把给a[i]乘的2的幂给a[j]乘。#include …

深入 Go 底层原理(二):Channel 的实现剖析

1. 引言"Do not communicate by sharing memory; instead, share memory by communicating." (不要通过共享内存来通信&#xff0c;而应通过通信来共享内存。) 这是 Go 语言并发设计的核心哲学。而 channel 正是实现这一哲学的核心工具。Channel 为 Goroutine 之间的…

Golang 语言的编程技巧之类型

1、介绍Golang 语言是一门静态类型的编程语言&#xff0c;我们在编写代码时&#xff0c;为了提升代码的灵活性&#xff0c;有时会使用空接口类型&#xff0c;对于空接口类型的变量&#xff0c;一般会通过类型断言判断变量的类型&#xff0c;而且可能还会遇到遇到类型转换的场景…

计数组合学7.11(RSK算法)

7.11 RSK算法 在对称函数理论中&#xff0c;有一个非凡的组合对应关系&#xff0c;称为RSK算法。&#xff08;关于缩写RSK的含义以及其他名称&#xff0c;请参阅本章末尾的注释。&#xff09;这里我们仅介绍RSK算法的最基本性质&#xff0c;从而能够给出舒尔函数一些基本性质的…

国产嵌入式调试器之光? RT-Trace 初体验!

做过嵌入式开发的工程师肯定都知道有这么个玩意儿 —— J-Trace&#xff0c;与我们日常使用的普通调试器不同点在于&#xff0c;它在基本的下载/调试代码之上还具有非常强大的代码运行跟踪能力&#xff0c;从而实现代码覆盖率的分析、指令回溯、CPU 资源监控等一系列强大的功能…

SLAM中的非线性优化-2D图优化之零空间实战(十六)

终于有时间更新实战篇了&#xff0c;本节实战几乎包含了SLAM后端的所有技巧&#xff0c;其中包括&#xff1a;舒尔补/先验Factor/鲁棒核函数/FEJ/BA优化等滑动窗口法的相关技巧&#xff0c;其中构建2D轮式里程计预积分以及绝对位姿观测的10帧滑动窗口&#xff0c;并边缘化最老帧…

知识随记-----Qt 实战教程:使用 QNetworkAccessManager 发送 HTTP POST

文章目录Qt 网络编程&#xff1a;使用 QNetworkAccessManager 实现 HTTP POST 请求概要整体架构流程技术名词解释技术细节注意事项&#xff1a;Qt 网络编程&#xff1a;使用 QNetworkAccessManager 实现 HTTP POST 请求 概要 本文介绍如何使用 Qt 框架的网络模块&#xff08;…

wordpress批量新建产品分类

1、下载安装插件&#xff1a;bulk-category-import-export2、激活插件后&#xff0c;左侧点击插件下的导入&#xff0c;选择product categories&#xff0c;点击下一步3、这里可以选择导入的分类列表文件&#xff0c;可以选择分隔符&#xff0c;CSV文件默认为‘&#xff0c;’要…

CentOS 镜像源配置与 EOL 后的应对策略

引言 本文将详细介绍如何使用 阿里云开源镜像站 配置 CentOS 的各类软件源&#xff0c;包括基础源、历史归档源&#xff08;vault&#xff09;、ARM 架构源、Stream 版本以及调试信息源&#xff08;debuginfo&#xff09;&#xff0c;并重点讲解在 CentOS 8 停止维护后&#x…

CTF实战:用Sqlmap破解表单输入型SQL注入题(输入账号密码/usernamepassword)

目录 引言 步骤1&#xff1a;用Burp Suite捕获表单请求 步骤2&#xff1a;用Sqlmap获取数据库名称 参数解释&#xff1a; 输出示例&#xff08;根据题目环境调整&#xff09;&#xff1a; 步骤3&#xff1a;获取目标数据库中的表名 参数解释&#xff1a; 输出示例&#…

质数时间(二分查找)

题目描述如果把一年之中的某个时间写作 a 月 b 日 c 时 d 分 e 秒的形式&#xff0c;当这五个数都为质数时&#xff0c;我们把这样的时间叫做质数时间&#xff0c;现已知起始时刻是 2022 年的 a 月 b 日 c 时 d 分 e 秒&#xff0c;终止时刻是 2022 年的 u 月 v 日 w 时 x 分 y…

Python训练Day29

浙大疏锦行 类的装饰器装饰器思想的进一步理解&#xff1a;外部修改、动态类方法的定义&#xff1a;内部定义和外部定义

新手DBA实战指南:如何使用gh-ost实现MySQL无锁表结构变更

新手DBA实战指南:如何使用gh-ost实现MySQL无锁表结构变更 作为DBA,大表结构变更(DDL)一直是令人头疼的问题。传统的ALTER TABLE操作会锁表,严重影响业务连续性;而常见的pt-online-schema-change工具虽然能实现在线变更,但依赖触发器机制,在高并发场景下性能表现不佳。本…

OSPF综合

一、实验拓扑二、实验需求1、R4为ISP&#xff0c;其上只配置IP地址&#xff1b;R4与其他所直连设备间均使用公有IP&#xff1b; 2、R3-R5、R6、R7为MGRE环境&#xff0c;R3为中心站点&#xff1b; 3、整个OSPF环境IP基于172.16.0.0/16划分&#xff1b;除了R12有两个环回&#x…

技术面试知识点详解 - 从电路到编程的全栈面经

技术面试知识点详解 - 从电路到编程的全栈面经 目录 模拟电路基础数字电路原理电源设计相关编程语言基础数据库与并发网络协议基础算法与数据结构 模拟电路基础 1. 放大电路类型判断 这是模拟电路面试的经典题目&#xff0c;通过电压放大倍数判断放大电路类型&#xff1a; …

LangGraph认知篇-Command函数

Command简述 在 LangGraph 中&#xff0c;Command 是一个极具实用性的功能&#xff0c;它能够将控制流&#xff08;边&#xff09;和状态更新&#xff08;节点&#xff09;巧妙地结合起来。这意味着开发者可以在同一个节点中&#xff0c;既执行状态更新操作&#xff0c;又决定下…

【目标检测】小样本度量学习

小样本度量学习&#xff08;Few-Shot Metric Learning&#xff09;通常用于分类任务​&#xff08;如图像分类&#xff09;&#xff0c;但它也可以与目标检测&#xff08;Object Detection&#xff09;结合&#xff0c;解决小样本目标检测&#xff08;Few-Shot Object Detectio…

cmd怎么取消关机命令

在 Windows 的命令提示符&#xff08;CMD&#xff09;中取消已计划的关机操作&#xff0c;可以通过 shutdown 命令的 ​**-a**​ 参数实现。以下是具体步骤&#xff1a;​操作方法​​打开 CMD​按下 Win R 组合键&#xff0c;输入 cmd 并回车&#xff0c;打开命令提示符窗口。…

网易云音乐硬刚腾讯系!起诉SM娱乐滥用市场支配地位

企查查APP显示&#xff0c;近日&#xff0c;法院公开杭州乐读科技有限公司、杭州网易云音乐科技有限公司起诉SM ENTERTAINMENT CO. 、卡斯梦&#xff08;上海&#xff09;文化传播有限公司等开庭信息&#xff0c;案由涉及滥用市场支配地位纠纷。公告显示&#xff0c;该案件计划…

[css]切角

使用css实现一个切角的功能&#xff0c;有以下几种方案&#xff1a; <div class"box"></div>方案一&#xff1a;linear-gradient linear-gradient配合backgroud-image可以实现背景渐变的效果。linear-gradient的渐变过渡区的占比是总的空间&#xff08;高…