线程同步与Mutex

梦想是逃离世界…

文章目录

  • 一、什么是线程同步?
  • 二、线程同步机制
  • 三、互斥锁(Mutex)
  • 四、loock 和 unlock
  • 五、Mutex的四种类型


一、什么是线程同步?

  • 线程同步(Thread Synchronization)是多线程编程中的一个重要概念,它指的是通过一定的机制来控制多个线程之间的执行顺序,以确保它们能够正确地访问和修改共享资源,从而避免数据竞争和不一致性问题。
  • 在多线程环境中,多个线程可能同时访问和修改共享资源(如变量,数据结构或文件等)。如果没有适当的同步机制,这些线程可能会以不可预测的顺序执行,导致数据竞争、脏读、脏写或其他不可预期的行为。线程同步的目标就是确保线程之间的有序执行,以维护数据的一致性和完整性。

为什么要同步呢?
如果在同一时刻,仅只有一个线程访问某个变量(临界资源),不会存在脏数据的问题,但是,如果同一时刻有多个线程,同时访问同一个临界资源的时候,就会存在 问题,如何来解决这个问题呢?这就提出了“线程同步”这个概念。

以下是一个简单的例子:
在这里插入图片描述

那么我们有没有什么办法,控制这些线程对临界资源的访问呢?想个什么办法才能使它们之间不乱套呢?这就该线程同步机制登场了

二、线程同步机制

C++中提供了多种线程同步机制,常用的方法包括:

  1. 互斥锁(Mutex):互斥锁是最常用的线程同步机制之一,当一个线程想要访问共享资源时,它首先会尝试获取与该资源关联的互斥锁如果锁已经被其他线程持有,则该线程将被阻塞,直到锁被释放。这样可以确保在任何时候只有一个线程能够访问共享资源。
  2. 条件变量(Condition Variable):条件变量用于使线程在满足某个条件之前等待,它通常与互斥锁一起使用,以便在等待条件成立时释放锁,并在条件成立时重新获取锁。这允许线程在等待期间不占用锁,从而提高并发性能。
  3. 信号量(Semaphore):信号量是一种通用的线程同步机制,它允许多个线程同时访问共享资源,但限制同时访问的线程数量。信号量内部维护一个计数器,用于表示可用资源的数量。当线程需要访问资源时,它会尝试减少计数器的值;当线程释放资源时,它会增加计数器的值。当计数器的值小于零时,尝试获取资源的线程将被阻塞。
  4. 原子操作(Atomic Operation):原子操作时不可中断的操作,即在执行过程中不会被其他线程打断。C++11及以后的版本提供了头文件,其中包含了一系列原子操作的函数和类。这些原子操作可以用于安全地更新共享数据,而无需是同互斥锁等同步机制。

三、互斥锁(Mutex)

互斥(Mutex)是一种同步机制,同于保护共享资源,放置多个线程同时访问和修改同一资源,从而引起数据竞争(data race)和不一致性。
当一个线程想要访问某个共享资源时,它首先会尝试获取与该资源关联的互斥锁(mutex)。如果互斥锁已经被其他线程持有(即被锁定),则该线程将被阻塞,直到互斥锁被释放(即被解锁)。一旦线程成功获取到互斥锁,它就可以安全地访问共享资源,并在访问完成后释放互斥锁,以便其他线程可以获取该锁并访问资源。
互斥锁通常具有以下几个特性:

  1. 互斥性:任意时刻只有一个线程可以持有某个互斥锁。
  2. 原子性:对互斥锁的获取和释放操作时原子的,即在执行这些操作时不会被其他线程打断。
  3. 可重入性:某些互斥锁类型(如递归锁)允许同一线程多次获取同一个锁,但通常不建议这样做,因为它会增加死锁的风险。
  4. 非阻塞性:虽然互斥锁本身是一种阻塞性同步机制,但某些高级实现(如尝试锁)允许线程在无法立即获取锁时继续执行其他任务,而不是被阻塞。
#include <iostream>
#include <thread>
using namespace std;
//共享变量,没有互斥锁或原子操作
int counter = 0;

//线程函数  对counter进行自增操作
void increment_counter(int times)
{
    for(int i=0;i<times;++i)
    {
        //这是一个数据竞争,因为多个线程可能同时执行这行代码
        counter++;
    }
}

int main()
{
    //创建两个线程,每个线程对counter自增100000次
    thread t1(increment_counter,100000);
    thread t2(increment_counter,100000);

    //等待两个线程完成
    t1.join();
    t2.join();

    //输出结果,这个结果可能不是200000
    cout<<"最终结果:"<<counter<<endl;

    return 0;
}

在以上的这个例子中,输出的结果可能不是我们想要的那个答案,这是因为我们创建了两个线程,这两个线程的同时占用counter这个资源,前一个线程刚修改counter的值,后一个线程可能就会立即覆盖掉当前这个counter的值。
接下来我们对这个例子进行改造:

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
//共享变量,没有互斥锁或原子操作
int counter = 0;

//定义一个互斥锁
mutex mymutex;

//线程函数  对counter进行自增操作
void increment_counter(int times)
{
    for(int i=0;i<times;++i)
    {
        mymutex.lock();//在访问临界资源之前先加锁
        counter++;
        mymutex.unlock();//访问完了之后解锁,把锁释放
    }
}

int main()
{
    //创建两个线程,每个线程对counter自增100000次
    thread t1(increment_counter,100000);
    thread t2(increment_counter,100000);

    //等待两个线程完成
    t1.join();
    t2.join();

    //输出结果
    cout<<"最终结果:"<<counter<<endl;

    return 0;
}

通过加上互斥锁之后,我们输出的结果将是正确的。

四、loock 和 unlock

在C++中,使用std::mutex的lock()和unlock()函数来管理对共享资源的访问,从而确保在多线程环境中资源的同步访问。
以下是关于如何使用它们以及需要注意的事项:

  1. 如何使用?
    首先,你需要创建一个std::mutex对象
std::mutex mymutex;

在访问共享资源之前,使用lock()函数锁定互斥量

mymutex.lock();
//访问共享变量
...............

在互斥锁被锁定期间,你可以安全的访问共享资源,因为其他试图锁定该互斥量的线程将被阻塞。
一旦完成对共享资源的访问,将会使用unlock()函数解锁互斥量。

//完成对共享资源的访问
mymutex.unlock();
  1. 注意事项
    (1)死锁:
    如果线程在持有互斥量的情况下调用了一个阻塞操作(如另一个互斥量的lock()),并且这个阻塞操作永远不会完成(因为其他线程持有它需要的资源),那么就会发生死锁,避免死锁的一种方法就是始终按照相同的顺序锁定互斥量,或者使用更高级的同步原语,如std::lock_guard或std::unique_lock,它们可以自动管理锁的获取和释放。
    (2)异常安全:
    如果在锁定互斥量之后抛出异常,那么必须确定互斥量被正确解锁,使用std::lock_guard或std::unique_lock可以自动处理这种情况,因为它们在析构时会释放锁。
    如下面这个例子:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

//全局互斥量
mutex mymutex;

void safe_function()
{
    std::lock_guard<mutex> lock(mymutex);//锁定互斥量
    //在这里执行需要互斥访问的代码
    //如果抛出异常,lock_guard会在析构时自动解锁mymutex
    try{
        //模拟一些可能抛出的异常
        if(/*some condition that might cause an exception*/){
            throw std::runtime_error("An error occurred");//使用抛出异常
        }
        //......其他代码块
    }
    catch(const std::exception& e){
        //处理异常,但不需要担心解锁,因为lock_guard会自动处理
        std::cerr << "Caught exception:"<<e.what() << '\n';
    }
    //lock_guard离开作用域时自动解锁mymutex

}

int main()
{
    //假设这里有一些线程调用safe_function()
    //由于使用了lock_guard,所以wu'lun是否抛出异常,mymutex都会被正确解锁
    //........
    
    return 0;
}

(3)不要手动解锁未锁定的互斥量:
在调用unlock()之前,必须确保互斥量已经被lock()锁定,否则,该行为是未定义的。
(4)不要多次锁定同一互斥量:
对于非递归互斥量(如std::mutex),不要再同一线程中多次锁定它。这会导致未定义的行为。如果需要递归锁定,请使用std::recursive_mutex。
(5)使用RAII管理锁
使用RAII(资源获取即初始化)原则来管理锁的生命周期,通过std::lock_guardhuostd::unique_lock来确保锁在不需要时自动释放。
(6)避免长时间持有锁
尽量缩短持有锁的时间,以减少线程之间的争用,提高程序的并发性能。
(7)考虑使用更高级的同步原语
除了std::mutex之外,C++标准库还提供了其他更高级的同步原语,如条件变量(std::condition_variable)、读写锁(std::shared_mutex)等,它们可以在特定场景下提供更高效的同步机制。

五、Mutex的四种类型

在C++中,特别是从C++11开始,std::mutex及其相关类型提供了一系列用于同步多线程访问共享资源的机制,以下时std::mutex的四种主要类型及其详细解释:

  1. std::mutex
    · 这是最基本的互斥量类型
    ·它不允许递归锁顶,即同一个线程不能被多次锁定同一个std::mutex,如果尝试这样做,程序的行为将是未定义的,通常会导致死锁。
    ·它提供了基本的锁定(lock)和解锁(unlock)操作
    ·当一个线程锁定了一个std::mutex时,任何其他尝试锁定该互斥量的线程都将被阻塞,直到原始线程调用unlock()释放它。
  2. std::recursive_mutex
    `这是一个递归(或可重入)互斥量
    ·与std::mutex不同,它允许同一线程多次锁定一个互斥量。这可以用于需要递归访问受保护资源的场景。
    ·线程在每次锁定时都需要对应的解锁,以确保正确的同步。
    ·如果线程没有正确匹配其锁定和解锁操作(即解锁次数少于锁定次数),则其他线程仍然会被阻塞。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

std::recursive_mutex mtx;

void recursive_function()
{
    mtx.lock();//第一次锁定
    cout<<"Thread "<<this_thread::get_id()<<" locked mutex"<<endl;

    //递归锁定
    mtx.lock();//同一线程可以多次锁定
    cout<<"Thread "<<this_thread::get_id()<<" locked mutex again"<<endl;

    mtx.unlock();//解锁一次
    cout<<"Thread "<<this_thread::get_id()<<" unlocked mutex"<<endl;

    mtx.unlock();//再次解锁
    cout<<"Thread "<<this_thread::get_id()<<" unlocked mutex again"<<endl;
}

int main()
{
    std::thread t1(recursive_function);

    t1.join();

    return 0;
}
  1. std::timed_mutex
    `这是一个带时限的互斥量
    ·除了提供基本的锁定和解锁操作外,它还允许线程尝试在一定时间内锁定互斥量。
    ·如果在指定时间内无法获取锁,try_lock_for()或try_lock_until()函数将返回失败,而线程则不会被阻塞。
    ·这对于实现有超时机制的资源访问非常有用。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

std::timed_mutex mtx;

void timed_lock_function() {
    auto start = std::chrono::high_resolution_clock::now();//高精度时间

    //尝试在指定时间内获取锁
    if(mtx.try_lock_for(std::chrono::seconds(2)))//加了一个2秒的等待时间
    {
        cout<<"Thread"<<this_thread::get_id()<<" get the lock"<<endl;
        std::this_thread::sleep_for(std::chrono::seconds(3));
        mtx.unlock();
        cout<<"Thread"<<this_thread::get_id()<<" release the lock"<<endl;
    }
    else
    {
        cout<<"Thread"<<this_thread::get_id()<<" can't get the lock"<<endl;
    }
    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double> diff = end-start;
    cout<<"Thread"<<this_thread::get_id()<<" cost "<<diff.count()<<"s"<<endl;
}

int main()
{
    std::thread t1(timed_lock_function);
    std::thread t2(timed_lock_function);

    t1.join();
    t2.join();
    return 0;
}
  1. std::recursive_timed_mutex:
    `这是以恶搞递归且带时限的互斥量
    ·它结合了std::recursive_mutex和std::timed_mutex的特性
    ·它允许同一线程多次锁定同一个互斥量,并提供了带时限的锁定尝试功能。
    ·这使得线程在需要递归访问资源且希望在一定时间内获取锁的场景中更加灵活。

在使用这些互斥量类型时,需要注意正确的管理锁定和解锁操作,以避免死锁和其他同步问题。同时,根据具体的应用场景和需求选择合适的互斥量类型也是非常重要的。

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

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

相关文章

基于SpringBoot和PostGIS的全球首都信息管理设计与实现

目录 前言 一、首都空间表的设计 1、三张空间表的结构 二、SpringBoot后台管理的设计与实现 1、模型层的实现 2、业务层及控制层实现 三、前端的实现与成果可视化 1、新增数据的保存 2、首都的实际管理成果 3、全球首都信息 四、总结 前言 首都&#xff0c;一个国家的…

计算机网络 (50)两类密码体制

前言 计算机网络中的两类密码体制主要包括对称密钥密码体制&#xff08;也称为私钥密码体制、对称密码体制&#xff09;和公钥密码体制&#xff08;也称为非对称密码体制、公开密钥加密技术&#xff09;。 一、对称密钥密码体制 定义&#xff1a; 对称密钥密码体制是一种传…

【数据结构篇】顺序表 超详细

目录 一.顺序表的定义 1.顺序表的概念及结构 1.1线性表 2.顺序表的分类 2.1静态顺序表 2.2动态顺序表 二.动态顺序表的实现 1.准备工作和注意事项 2.顺序表的基本接口&#xff1a; 2.0 创建一个顺序表 2.1 顺序表的初始化 2.2 顺序表的销毁 2.3 顺序表的打印 3.顺序…

C 语言雏启:擘画代码乾坤,谛观编程奥宇之初瞰

大家好啊&#xff0c;我是小象٩(๑ω๑)۶ 我的博客&#xff1a;Xiao Xiangζั͡ޓއއ 很高兴见到大家&#xff0c;希望能够和大家一起交流学习&#xff0c;共同进步。* 这一课主要是让大家初步了解C语言&#xff0c;了解我们的开发环境&#xff0c;main函数&#xff0c;库…

根据 Web 服务器端的架构相关知识,将PHP改JAVA重构企业网站系统

目录 案例 【题目】 【问题 1】(7 分) 【问题 2】(8 分) 【问题 3】(10 分) 答案 【问题 1】解析 【问题 2】解析 【问题 3】解析 相关推荐 案例 阅读以下关于应用服务器的叙述&#xff0c;在答题纸上回答问题 1 至问题 3。 【题目】 某电子产品制造公司&#xff0c…

多选multiple下拉框el-select回显问题(只显示后端返回id)

首先保证v-model的值对应options数据源里面的id <el-form-item prop"subclass" label"分类" ><el-select v-model"formData.subclass" multiple placeholder"请选择" clearable :disabled"!!formData.id"><e…

java快速导出word文档

点关注不迷路&#xff0c;欢迎再访&#xff01; 精简博客内容&#xff0c;尽量已行业术语来分享。 努力做到对每一位认可自己的读者负责。 帮助别人的同时更是丰富自己的良机。 文章目录 前言一.添加 Apache POI 依赖二.填充文档内容三.导出文档效果测试 前言 在 Java 应用程序…

《MambaIR:一种基于状态空间模型的简单图像修复基线方法》学习笔记

paper&#xff1a;2402.15648 目录 摘要 一、引言 1、模型性能的提升依赖于网络感受野的扩大&#xff1a; 2、全局感受野和高效计算之间存在固有矛盾&#xff1a; 3、改进版 Mamba的巨大潜力 4、Mamba 在图像修复任务中仍面临以下挑战&#xff1a; 5、方法 6、主要贡献…

ngnix上传小文件成功,大文件报错

ngnix错误日志 "/var/tmp/nginx/client//0000001299" failed (2: No such file or directory), client: 10.188.141.160, server: 127.0.0.1, request: "POST /fts/sys/common/biUpload HTTP/1.1", host: "10.20.166.179", referrer: "http…

Word表格批量提取数据到Excel,批量提取,我爱excel

Word表格批量提取数据到Excel&#xff0c;Word导出到Excel - 我爱Excel助你高效办公 在日常办公中&#xff0c;Word表格常常用于记录和整理数据&#xff0c;但将这些数据从Word提取到Excel&#xff0c;特别是当涉及多个文件时&#xff0c;常常让人头疼。如果你经常需要将多个W…

【Zookeeper】Windows下安装Zookeeper(图文记录详细步骤,手把手包安装成功)

【Zookeeper】Windows下安装Zookeeper Zookeeper简介一、下载Zookeeper安装包1.1、官网下载Zookeeper1.2、网盘下载Zookeeper 二、解压Zookeeper安装包到指定目录三、Zookeeper安装目录下创建文件夹四、进入config目录五、复制zoo_sample.cfg文件&#xff0c;改名为zoo.cfg六、…

JDK长期支持版本(LTS)

https://blogs.oracle.com/java/post/the-arrival-of-java-23 jdk长期支持版本&#xff08;LTS&#xff09;&#xff1a;JDK 8、11、17、21&#xff1a;

深度学习J3周:RNN-心脏病预测

&#x1f368; 本文为&#x1f517;365天深度学习训练营中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 要求&#xff1a; 1.本地读取并加载数据 2.了解循环神经网络&#xff08;RNN&#xff09;的构建过程 3.测试集accuracy到达87% 拔高&#xff1a; 测试机a…

Linux C\C++方式下的文件I/O编程

【图书推荐】《Linux C与C一线开发实践&#xff08;第2版&#xff09;》_linux c与c一线开发实践pdf-CSDN博客 《Linux C与C一线开发实践&#xff08;第2版&#xff09;&#xff08;Linux技术丛书&#xff09;》(朱文伟&#xff0c;李建英)【摘要 书评 试读】- 京东图书 Lin…

FPGA:Quartus软件与操作系统版本对照表

文章目录 1.软件概述2.软件版本3.设计流程4.支持的设备5.新特性6.版本对照 1.软件概述 Quartus软件是由英特尔&#xff08;Intel&#xff09;公司开发的一款功能强大的FPGA&#xff08;现场可编程逻辑门阵列&#xff09;设计工具&#xff0c;广泛应用于数字电路设计、仿真、综…

ui设计公司分享:浅色 UI 设计

在数字化产品琳琅满目的今天&#xff0c;用户对于界面的要求早已不止于功能的实现&#xff0c;更追求一种舒适、无压的交互体验。而浅色UI设计&#xff0c;凭借其独特的魅力&#xff0c;正逐渐成为众多设计师营造优质体验的首选。 一、浅色UI设计的视觉优势 &#xff08;一&a…

Unity中实现伤害跳字效果(简单好抄)

第一步骤安装并导入Dotween插件&#xff08;也可以不用导入之后直接下载我的安装包&#xff09; 官网DOTween - 下载 第二步&#xff1a; 制作跳字预制体 建议把最佳适应打开&#xff0c;这样就不怕数字太大显示不全了。 第三步&#xff1a;创建一个空对象并编写脚本JumpNumbe…

为什么相关性不是因果关系?人工智能中的因果推理探秘

目录 一、背景 &#xff08;一&#xff09;聚焦当下人工智能 &#xff08;二&#xff09;基于关联框架的人工智能 &#xff08;三&#xff09;基于因果框架的人工智能 二、因果推理的基本理论 &#xff08;一&#xff09;因果推理基本范式&#xff1a;因果模型&#xff0…

VMamba 安装教程(无需更改base环境中的cuda版本)

导航 安装教程导航 Mamba 及 Vim 安装问题参看本人博客&#xff1a;Mamba 环境安装踩坑问题汇总及解决方法&#xff08;初版&#xff09;Linux 下Mamba 及 Vim 安装问题参看本人博客&#xff1a;Mamba 环境安装踩坑问题汇总及解决方法&#xff08;重置版&#xff09;Windows …

FRP内网穿透0.61.1新版教程

在上一篇zerotier讲述了如何实现虚拟局域网搭建&#xff0c;这篇会讲述FRP内网穿透的使用教程 那么frp与zerotier的区别是什么呢&#xff1f;&#xff08;说人话&#xff09; FRP 主要用于内网服务向外网的单向暴露。 ZeroTier 用于构建一个虚拟的私有网络&#xff0c;实现多点…