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

【C++】多线程同步三剑客介绍

目录

条件变量

头文件

主要操作函数

1、等待操作

2、唤醒操作

使用示例

信号量

头文件

主要操作函数

1、信号量初始化

2、等待操作(P操作)

3、信号操作(V操作)

4、获取信号量值 

5、销毁信号量

使用示例

互斥锁

头文件

使用示例


当我们需要给多个线程的指定执行顺序的时候,我们通常有多种方法:

  • 条件变量
  • 信号量
  • 互斥锁

在这篇文章里,会介绍如何使用这三种方式来为多个线程指定执行顺序,以及在使用的时候需要主义的地方。

条件变量

        条件变量是C++11引入的同步原语,用于在多线程环境中实现线程间的等待和通知机制。它允许一个或多个线程等待某个条件成立,当条件满足时,其他线程可以通知等待的线程继续执行,一般需要配合unique_lock使用。

头文件

#include <condition_variable>

主要操作函数

1、等待操作

a)基本形式

void wait(std::unique_lock<std::mutex>& lock);

b)带谓词形式

template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);

 两者的区别在于处理虚假唤醒的情况比较明显,这个在后面介绍哈。

2、唤醒操作

a)唤醒单个线程

void notify_one() noexcept;

特点:唤醒等待队列中的一个线程,具体是哪个线程是未定义的

b)唤醒所有线程

void notify_all() noexcept;

特点:唤醒等待队列中的所有线程,性能开销比较大,但是确保所有等待线程都被唤醒。

这里需要介绍一下虚假唤醒的问题

        虚假唤醒是指线程在没有收到notify_one或者notify_all调用的情况,从wait状态中被唤醒。为什么会出现虚假唤醒的情况呢?因为可能会出现系统信号中断条件变量的等待(SIGINT),或者因为底层I/O操作等底层系统调用中断,导致pthread_cond_wait() 被中断返回,因此出现虚假唤醒的情况。

        在上面,我们介绍了两种等待的方式,他们在处理虚假唤醒的情况表现有所不同。

        带谓词的等待方式,会自动处理虚假唤醒,不需要我们再进行手动处理,那么他是怎么做到自动处理的呢,他的内部实现等价如下代码,就是在循环中不断判断条件是否满足,以此来处理虚假唤醒的情况。

// 带谓词的wait()函数的内部实现等价于:
template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred) {while (!pred()) {        // 关键:自动循环检查wait(lock);          // 调用基本的wait()}// 退出循环时,保证 pred() 返回 true
}

        基本的等待方式 需要我们手动处理虚假唤醒的情况,如下代码是有问题的:

void wrong_basic_wait() {std::unique_lock<std::mutex> lock(mtx);// 错误:只等待一次,不处理虚假唤醒cv.wait(lock);// 假设条件一定满足 - 危险!if (data_ready) {process_data();}
}

        如果因为底层系统调用中断了等待,但是此时条件并不满足,比如数据并未准备好,会出现未定义的情况,因此,我们需要模仿带谓词的等待方式的等价写法,在循环中判断,如下:

void correct_basic_wait() {std::unique_lock<std::mutex> lock(mtx);// 正确:使用循环处理虚假唤醒while (!condition_satisfied()) {cv.wait(lock);// 如果是虚假唤醒,循环会继续等待// 如果条件真的满足,循环会退出}// 这里保证条件一定满足process_data();
}

使用示例

1114. 按序打印 - 力扣(LeetCode)https://leetcode.cn/problems/print-in-order/description/

class Foo {condition_variable m_cv;mutex m_mtx;int m_nFlg;
public:Foo() {m_nFlg=1;}void first(function<void()> printFirst) {// printFirst() outputs "first". Do not change or remove this line.unique_lock<mutex> lock(m_mtx);m_cv.wait(lock,[=](){return m_nFlg==1;});printFirst();m_nFlg=2;m_cv.notify_all();}void second(function<void()> printSecond) {// printSecond() outputs "second". Do not change or remove this line.unique_lock<mutex> lock(m_mtx);m_cv.wait(lock,[=](){return m_nFlg==2;});printSecond();m_nFlg=3;m_cv.notify_all();}void third(function<void()> printThird) {// printThird() outputs "third". Do not change or remove this line.unique_lock<mutex> lock(m_mtx);m_cv.wait(lock,[=](){return m_nFlg==3;});printThird();m_nFlg=1;m_cv.notify_all();}
};

信号量

信号量的本质就是一个非负整数计数器,支持两个原子操作:P(等待/减少)、V(信号/增加)

头文件

#include <semaphore.h>

主要操作函数

1、信号量初始化

int sem_init(sem_t *sem, int pshared, unsigned int value);

参数说明:

  • sem:指向信号量(sem_t)的指针
  • pshared:0表示线程间共享,非0表示进程间共享
  • value:信号量的初始值

返回值

  • 返回0:初始化成功
  • 返回-1:初始化失败,同时设置errno的错误码。

2、等待操作(P操作)

信号量等待有三种方式

a)sem_wait()-阻塞等待

int sem_wait(sem_t *sem);

特点:如果信号量值为0,线程会一直阻塞等待,知道信号量可用。

b)sem_trywait()-非阻塞等待

int sem_trywait(sem_t *sem);

特点:非阻塞等待,立即返回,不会等待,如果信号量不可用,立即返回-1,不会造成线程阻塞的情况,适用于轮询场景

c)sem_timedwait() - 超时等待

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

特点:在指定时间内等待,超时后返回-1,使用绝对时间戳,不是相对时间。

这个参数比较多,这里演示下用法:

struct timespec结构体用于存储超时时间:

  • tv_sec:秒数
  • tv_nsec:纳秒数
#include <semaphore.h>
#include <iostream>
#include <time.h>
#include <errno.h>void timed_work() {struct timespec timeout;clock_gettime(CLOCK_REALTIME, &timeout);timeout.tv_sec += 5;  // 5秒后超时int result = sem_timedwait(&sem, &timeout);if (result == 0) {std::cout << "在超时前获取到信号量" << std::endl;// 执行临界区代码sem_post(&sem);} else {if (errno == ETIMEDOUT) {std::cout << "等待超时,放弃获取" << std::endl;}}
}

 下面的代码作用是获取当前的系统时间,CLOCK_REALTIME表示使用系统实时时钟

clock_gettime(CLOCK_REALTIME, &timeout);

3、信号操作(V操作)

释放信号量,也就是将信号量的值+1。

int sem_post(sem_t *sem);

4、获取信号量值 

int sem_getvalue(sem_t *sem, int *sval);

5、销毁信号量

这种只能用于未命名的信号量,比如我们直接定义的sem_t sem,就属于未命名信号量

int sem_destroy(sem_t *sem);

使用示例

1114. 按序打印 - 力扣(LeetCode)https://leetcode.cn/problems/print-in-order/description/这题希望我们指定三个线程的执行顺序,我们可以定义三个信号量来进行控制

class Foo {sem_t s1,s2,s3;
public:Foo() {sem_init(&s1,0,1);sem_init(&s2,0,0);sem_init(&s3,0,0);}~Foo() {sem_destroy(&s1);sem_destroy(&s2);sem_destroy(&s3);}void first(function<void()> printFirst) {// printFirst() outputs "first". Do not change or remove this line.sem_wait(&s1);printFirst();sem_post(&s2);}void second(function<void()> printSecond) {// printSecond() outputs "second". Do not change or remove this line.sem_wait(&s2);printSecond();sem_post(&s3);}void third(function<void()> printThird) {// printThird() outputs "third". Do not change or remove this line.sem_wait(&s3);printThird();sem_post(&s1);}
};

互斥锁

头文件

#include <mutex>

使用示例

因为互斥锁比较简单这里,直接展示使用示例:1114. 按序打印 - 力扣(LeetCode)https://leetcode.cn/problems/print-in-order/

class Foo {mutex mtx1,mtx2,mtx3;
public:Foo() {mtx2.lock();mtx3.lock();}void first(function<void()> printFirst) {// printFirst() outputs "first". Do not change or remove this line.mtx1.lock();printFirst();mtx2.unlock();}void second(function<void()> printSecond) {// printSecond() outputs "second". Do not change or remove this line.mtx2.lock();printSecond();mtx3.unlock();}void third(function<void()> printThird) {// printThird() outputs "third". Do not change or remove this line.mtx3.lock();printThird();mtx1.unlock();}
};

http://www.lryc.cn/news/586782.html

相关文章:

  • 代码随想录算法训练营第十七天
  • 【C++】第十五节—一文详解 | 继承
  • JVM 垃圾收集算法全面解析
  • DC-DC变换器最基本拓扑 -Buck电路和Boost电路
  • ROS2---NodeOptions
  • MacOS使用Multipass快速搭建轻量级k3s集群
  • mac上BRPC的CMakeLists.txt优化:解决Protobuf路径问题
  • TensorFlow深度学习实战(24)——变分自编码器详解与实现
  • Vue 3 动态ref问题
  • 封装---统一封装处理页面标题
  • C++模版编程:类模版与继承
  • Qt 3D模块加载复杂模型
  • vue应用如何实现在 A 标签页登出,希望 B 标签页也自动感知并退出登录
  • 语音识别的速度革命:从 Whisper 到 Whisper-CTranslate2,我经历了什么?
  • 数据库3.0
  • HarmonyOS-ArkUI Web控件基础铺垫1-HTTP协议-数据包内容
  • EPLAN多项目并行,电气设计许可如何不浪费?
  • (S4)Efficiently Modeling Long Sequences with Structured State Spaces论文精读(逐段解析)
  • ReAct论文解读(1)—什么是ReAct?
  • 基于YOLOv11的无人机目标检测实战(Windows环境)
  • Spring Cloud Gateway 实战指南
  • 力扣经典算法篇-21- 两数之和 II - 输入有序数组(固定一端 + 二分查找法,双指针法)
  • MongoDB性能优化实战指南:原理、实践与案例
  • git实际工作流程
  • 【零基础入门unity游戏开发——unity3D篇】3D光源之——unity反射和反射探针技术
  • SPSSPRO:数据分析市场SaaS挑战者的战略分析
  • 深入解析Hadoop架构设计:原理、组件与应用
  • (2)从零开发 Chrome 插件:实现 API 登录与本地存储功能
  • 代码随想录|图论|14有向图的完全可达性
  • 基于Prompt结构的语校解析:3H日本语学校信息建模实录(4/500)