线程同步与Mutex

news/2025/3/15 22:46:51/

梦想是逃离世界…

文章目录

  • 一、什么是线程同步?
  • 二、线程同步机制
  • 三、互斥锁(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();//输出结果,这个结果可能不是200000cout<<"最终结果:"<<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会在析构时自动解锁mymutextry{//模拟一些可能抛出的异常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的特性
    ·它允许同一线程多次锁定同一个互斥量,并提供了带时限的锁定尝试功能。
    ·这使得线程在需要递归访问资源且希望在一定时间内获取锁的场景中更加灵活。

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


http://www.ppmy.cn/news/1564911.html

相关文章

基于微信小程序的安心陪诊管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

Golang学习笔记_27——单例模式

Golang学习笔记_24——泛型 Golang学习笔记_25——协程Golang学习笔记_25——协程 Golang学习笔记_26——通道 文章目录 单例模式1. 介绍2. 应用场景3. 实现3.1 饿汉式3.2 懒汉模式 源码 单例模式 1. 介绍 单例模式是一种创建型设计模式&#xff0c;它确保一个类只有一个实例…

Visual Studio Code + Stm32 (IAR)

记录一下&#xff0c; 以前看别人在 vsc 下配置 stm32 工程非常麻烦。 最近&#xff0c;突然发现&#xff0c; iar 官方出了两个插件&#xff0c; iar build 、 iar C-Spy 安装之后&#xff0c;配置一下 iar 软件路径。 然后&#xff0c;直接打开工程目录&#xff0c;编译…

一文夯实垃圾收集的理论基础

如何判断一个引用是否存活 引用计数法 给对象中添加一个引用计数器&#xff0c;每当有一个地方引用它&#xff0c;计数器就加 1&#xff1b;当引用失效&#xff0c;计数器就减 1&#xff1b;任何时候计数器为 0 的对象就是不可能再被使用的。 优点&#xff1a;可即刻回收垃圾&a…

第十二章:算法与程序设计

文章目录&#xff1a; 一&#xff1a;基本概念 1.算法与程序 1.1 算法 1.2 程序 2.编译预处理 3.面向对象技术 4.程序设计方法 5.SOP标志作业流程 6.工具 6.1 自然语言 6.2 流程图 6.3 N/S图 6.4 伪代码 6.5 计算机语言 二&#xff1a;程序设计 基础 1.常数 …

抖音小程序一键获取手机号

前端代码组件 <button v-if"!isFromOrderList"class"get-phone-btn" open-type"getPhoneNumber"getphonenumber"onGetPhoneNumber">一键获取</button>// 获取手机号回调onGetPhoneNumber(e) {var that this tt.login({f…

element-plus中的table为什么相同的数据并没有合并成一个

我想把所有的第一列的名字相同的内容合并。我发现只有相邻的数据合并了。实际上我想做到的是所有的后端给的数据&#xff0c;不管他的顺序怎样的&#xff0c;只有deviceTypeName 一样的都合并的。 在 element-plus 的 table 中&#xff0c;数据合并行通常是基于相邻行的数据进行…

ASP.NET Core全球化与本地化:打造多语言应用

一、引言 在经济全球化浪潮的席卷下&#xff0c;软件应用的受众早已突破地域限制&#xff0c;走向全球各个角落。为了满足不同地区用户的语言需求&#xff0c;ASP.NET Core 应用开发中&#xff0c;全球化与本地化的实现显得尤为关键。全球化旨在让应用程序具备适应不同国家和地…