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

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.dtcms.com/a/305584.html

相关文章:

  • HTML5 Web 存储
  • 从结构到交互:HTML5进阶开发全解析——语义化标签、Canvas绘图与表单设计实战
  • 【探索进程信号】:信号捕捉
  • iOS 签名证书与上架流程详解,无 Mac 环境下的上架流程
  • 微服务的编程测评系统8-题库管理-竞赛管理
  • 基于Rust与HDFS、YARN、Hue、ZooKeeper、MySQL
  • 【Dolphinscheduler】docker搭建dolphinscheduler集群并与安全的CDH集成
  • C++菱形虚拟继承:解开钻石继承的魔咒
  • 【ee类保研面试】数学类---线性代数
  • 智能车辆热管理测试方案——提升效能与保障安全
  • 设计模式之单例模式及其在多线程下的使用
  • 无人机磁力计模块运行与技术要点!
  • 企业级应用安全传输:Vue3+Nest.js AES加密方案设计与实现
  • 工作笔记-----FreeRTOS中的lwIP网络任务为什么会让出CPU
  • 【网络运维】 Linux:使用 Cockpit 管理服务器
  • Python 程序设计讲义(46):组合数据类型——集合类型:集合间运算
  • [25-cv-08377]Hublot手表商标带着14把“死神镰刀“来收割权!卖家速逃!
  • pyRoboPlan中的微分逆运动学
  • 手撕设计模式——智能家居之外观模式
  • Java Ai For循环 (day07)
  • .NET 10 中的新增功能系列文章2——ASP.NET Core 中的新增功能
  • Linux基本指令,对路径的认识
  • Power Pivot 数据分析表达式(DAX)
  • 【从基础到实战】STL string 学习笔记(上)
  • 文心大模型4.5开源:国产AI的破茧时刻与技术普惠实践
  • 梳理Ego-Planner模式下5通道、6通道与无人机模式的关系
  • 我的世界之战争星球 暮色苍茫篇 第二十五章、娜迦,卒
  • 观远 ChatBI 完成 DeepSeek-R1 大模型适配:开启智能数据分析跃升新篇
  • Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
  • 用Python+MySQL实战解锁企业财务数据分析