C++并发:并发操作的同步

有时我们不仅要共享数据,也要让独立线程上的行为同步。例如,某线程只有先等待另一线程的任务完成,才可以执行自己的任务。

C++提供了处理工具:条件变量future

并且进行了扩充:线程闩(latch),线程卡(barrier)

1 等待事件或等待其他条件

如果线程甲需要等待线程乙完成任务,可以采取几种不同的方式:

方式一:在共享数据内部维护一标志(受互斥保护),线程乙完成任务后,就设置标志成立。

该方式存在双重浪费:

1 线程甲须不断检查标志,耗费资源。

2 互斥一旦被锁住,其他线程无法再加锁(包括想要设置标志成立时的线程乙)。

方式二:让线程甲调用std::this_thread::sleep_for(),在各次查验之间短期休眠。

#include <mutex>
#include <thread>
bool flag;
std::mutex m;
void wait_for_flag() {std::unique_lock<std::mutex> lk(m);while (!flag) {lk.unlock();std::this_thread::sleep_for(std::chrono::milliseconds(100));lk.lock();}
}

让线程释放锁并休眠,让别的线程获取锁,这种写死的时间很难确定合适的值,而且合适的值也可能会随着系统运行而动态变化。

方式三:使用C++标准库的工具等待事件发生,优先使用这种方式。

比如条件变量。

1.1 使用条件变量来等待条件成立

两种条件变量的实现:

std::condition_variable和std::condition_variable_any。在头文件<condition_variable>内声明。

std::condition_variable仅限于和std::mutex一起使用,有更好的性能。

std::condition_variable_any可以和足以充当互斥的任一类型配合使用,更加通用,但是可能产生额外开销。

1.1.1 std::condition_variable

#include <condition_variable>
#include <mutex>
#include <queue>
std::mutex mut;
std::queue<data_chunk> data_queue;
std::condition_variable data_cond;void data_preparation_thread() {while (more_data_to_prepare()) {data_chunk const data = prepare_data();{std::lock_guard<std::mutex> lk(mut);data_queue.push(data);}data_cond.notify_one();}
}void data_processing_thread() {while (true) {std::unique_lock<std::mutex> lk(mut);data_cond.wait(lk, []{return !data_queue.empty();});data_chunk data=data_queue.front();data_queue.pop();lk.unlock();process(data);if (is_lask_chunk(data)) {break;}}
}

线程data_preparation_thread,在数据准备完成后,使用条件变量的notify_one(),通知一个正在等待的条件变量。

线程data_processing_thread,正在wait的条件变量收到通知后,继续往下进行,处理完成后对lk解锁(来让其他线程能够获取锁),这种需要加解锁灵活性的场景,让我们在这里选择了unique_lock而不是lock_guard。

1.1.2 伪唤醒

伪唤醒:如果线程data_processing_thread重新获得互斥,并且查验条件,但是这个行为不是直接响应线程data_preparation_thread的通知,就是伪唤醒。

这种伪唤醒出现的数量和频率都不确定。因此,若判定函数有副作用,则不建议选取它来查验条件。如果真的要这么做,有可能产生多次副作用。例如:每次被调用时提升线程优先级,多次伪唤醒可以使线程优先级非常高。

1.1.3 std::condition_variable::wait()

本质上是忙等的优化。

1.2 使用条件变量构建线程安全的队列

#include <queue>
#include <memory>
#include <mutex>
#include <condition_variable>template<typename T>
class threadsafe_queue {
private:mutable std::mutex mut;std::queue<T> data_queue;std::condition_variable data_cond;public:threadsafe_queue(){}threadsafe_queue(threadsafe_queue const& other) {std::lock_guard<std::mutex> lk(other.mut);data_queue=other.data_queue;}void push(T new_value) {std::lock_guard<std::mutex> lk(mut);data_queue.push(new_value);data_cond.notify_one();}void wait_and_pop(T& value) {std::unique_lock<std::mutex> lk(mut);data_cond.wait(lk, [this]{return !data_queue.empty();});value = data_queue.front();data_queue.pop();}std::shared_ptr<T> wait_and_pop() {std::unique_lock<std::mutex> lk(mut);data_cond.wait(lk, [this]{return !data_queue.empty();});std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));data_queue.pop();return res;}bool try_pop(T& value) {std::lock_guard<std::mutex> lk(mut);if (data_queue.empty()) {return false;}value = data_queue.front();data_queue.pop();return true;}std::shared_ptr<T> try_pop() {std::lock_guard<std::mutex> lk(mut);if (data_queue.empty()) {return std::shared_ptr<T>();}std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));data_queue.pop();return res;}bool empty() const {std::lock_guard<std::mutex> lk(mut);return data_queue.empty();}
};

2 使用future等待一次事件发生

C++标准库使用future模拟一次性事件:若线程等待某个特定的一次性事件发生,则会以一个恰当的方式获取一个future,他代表目标时间;接着,该线程就能一边执行其他任务,一边在future上等待;同时,他以短暂的间隔反复查验目标事件是否已经发生。

这个线程也可以转换运行模式:先不等目标事件发生,直接暂缓当前任务,切换到别的任务,必要时再回头等待future准备就绪。

future可能与数据关联,也可能未关联,一旦目标事件发生,其future即进入就绪状态,无法重置。

C++标准库有两种future:独占future(std::future<>)和共享future(std::shared_future<>)他们的设计参照了unique_ptr和shared_ptr。同一个事件仅仅允许关联唯一一个std::future实例,但是可以关联多个shared_future实例。

future能用于线程间通信,但是本身不提供同步访问,若多个线程需要同时访问一个future对象,需要使用互斥或其他同步方式。

2.1 从后台任务返回值std::async

只要我们并不急需线程运算的值,就可以使用std::async()异步方式启动任务。我们从std::async()函数处获得std::future对象(而非std::thread对象),运行的函数一旦完成,其返回值就由该对象最后持有。若要用到这个值,只需再future对象上调用get(),当前线程就会阻塞,以便future准备妥当并返回该值。

#include <future>
#include <iostream>
int find_the_answer_to_ltuae();
void do_other_stuff();int main() {std::future<int> the_answer = std::async(find_the_answer_to_ltuae);do_other_stuff();std::cout << "the answer is " << the_answer.get() << std::endl;
}

std::async的第一个参数是函数指针,第二个是用在调用函数之上的参数,其余类推。

如果std::async的参数是右值,则通过移动原始参数构建副本。

#include <string>
#include <future>struct X {void foo(int, std::string const &);std::string bar(std::string const &);
};X x;调用了p->foo(42, "hello");,其中p的值是&x
auto f1 = std::async(&X::foo, &x, 42, "hello");
调用了tempx.bar("goodbye");,其中tempx是x的副本
auto f2 = std::async(&X::bar, x, "goodbye");struct Y {double operator() (double);
};Y y;
调用tmpy(3.141)。其中由Y()生成的一个匿名变量传递给std::async(),进而发生移动构造。
在std::async()内部产生对象tmpy,在tmpy上执行Y::operator()(3.141)
auto f3 = std::async(Y(), 3.141);
调用y(2.718);
auto f4 = std::async(std::ref(y), 2.718);X baz(X&);
// 调用baz(x)
// std::async(baz, std::ref(x));class move_only {
public:move_only();move_only(move_only&&);move_only(move_only const&) = delete;move_only& operator=(move_only&&);move_only& operator=(move_only const&) = delete;void operator() ();
};
调用tmp(),其中tmp等价于std::move(move_only());
它的产生过程与std::async(Y(), 3.141);类似
auto f5 = std::async(move_only());
运行新线程
auto f6 = std::async(std::launch::async, Y(), 1.2);
在wait或get内部运行任务函数
auto f7 = std::async(std::launch::deferred, baz, std::ref(x));
交由实现自行选择运行方式
auto f8 = std::async(std::launch::deferred | std::launch::async, baz, std::ref(x));
交由实现自行选择运行方式
auto f9 = std::async( baz, std::ref(x));签名f7的任务函数调用被延后,到这里运行
f7.wait();

2.2 关联future实例和任务std::packaged_task<>

std::packaged_task<>连结了future对象函数。std::package_task<>对象在执行任务时,会调用关联的函数(或可调用对象),把返回值保存为future的内部数据,并令future准备就绪。

类模板std::package_task<>具有成员函数get_future(),它返回std::future<>实例,该future的特化类型取决于函数签名所指定的返回值。

std::package_task<>还具备函数调用操作符,他的参数取决于函数签名的参数列表。

#include <deque>
#include <mutex>
#include <future>
#include <thread>
#include <utility>std::mutex m;std::deque<std::packaged_task<void()>> tasks;
bool gui_shutdown_message_received();
void get_and_process_gui_message();
void gui_thread() {while (!gui_shutdown_message_received()) {get_and_process_gui_message();std::packaged_task<void()> task;{std::lock_guard<std::mutex> lk(m);if (tasks.empty()) {continue;}task = std::move(tasks.front());task.pop_front();}task();}
}std::thread gui_bg_thread(gui_thread);template<typename Func>
std::future<void> post_task_for_gui_thread(Func f) {std::packaged_task<void()> task(f);std::future<void> res = task.get_future();std::lock_guard<std::mutex> lk(m);tasks.push_back(std::move(task));return res;
}

2.3 创建std::promise

有些任务无法以简单的函数调用表达,还有一些任务的执行结果可能来自多个部分的代码。这种时候就需要用到std::promise显示异步求值。

在处理大量网络连接时,通常使用少量线程处理,来避免大量线程带来的上下文切换开销等。

配对的std::promise和std::future可实现下面的工作机制:等待数据的线程在future上阻塞,而提供数据的线程利用相配的promise设定关联的值,使future准备就绪。

若需从给定的std::promise实例获取关联的std::future对象,调用前者的get_future()即可。promise的值通过成员函数set_value()设置,只要设置好,future就准备就绪。

#include <future>
void process_connections(connection_set& connections) {while (!done(connections)) {for (connection_iterator connection=connections.begin(), end=connections.end(); connection!= end; ++connection) {if (connection->has_incoming_data()) {data_packet data = connection->incoming();std::promise<payload_type>& p = connection->get_promise(data.id);p.set_value(data.payload);}if (connection->has_outgoing_data()) {outgoing_packet data = connection->top_of_outgoing_queue();connection->send(data.payload);data.promise.set_value(true);}}}
}

2.4 将异常保存到future中

经由std::async()调用抛出的异常被保存到future中,等到get()调用,存储在内的异常会被抛出。

任务包装在packaged_task也是如此。std::promise也有这个功能,使用set_expection()。

#include <future>
extern std::promise<double> some_promise;
try {some_promise.set_value(value());
} catch (...) {some_promise.set_exception(std::current_exception());
}

2.5 多个线程同时等待std::shared_future

若在多个线程上访问同一个std::future,不采取措施会发生抢占。std::shared_future则可以解决这个问题,他可以复制出副本,但是它们全指向同一异步任务的状态数据。

std::shared_future的实例依据std::future的实例构造而得,前者的异步状态由后者决定。由于std::future独占异步状态,因此想要创建shared_future,需要使用std::move向其默认构造函数传递归属权。

#include <assert.h>
#include <future>
std::promise<int> p;
std::future<int> f(p.get_future());
// assert(f.valid());
std::shared_future<int> sf(std::move(f));隐式的归属权转换
std::shared_future<int> sf(p.get_future());

转移给sf后,对象f不再有效。

3 限时等待

有两种超时机制:

1 迟延超时,线程根据指定的时长而继续等待。

2 绝对超时,在某特定时间点来临之前,线程一直等待。

3.1 时钟类

使用std::chrono::system_clock::now()来获取系统当前时刻。

若时钟类每秒计数25次,那么表示为std::ratio<1, 25>

若时钟类每2.5秒计数1次,那么表示为std::ratio<5, 2>

std::chrono::steady_clock:恒温时钟类

std::chrono::system_clock:系统时钟类

std::chrono::high_resolution_clock:高精度时钟类

3.2 时长类

std::chrono::duration<>,是类模板,具有两个模板参数,前者指明采用何种类型表示计时单元的数量,后者是一个分数,设定该时长类的每一个计时单元代表多少秒。

例如:采用short值计数的分钟时长类:

std::chrono::duration<short, std::ratio<60, 1>>(1分钟60秒)

采用double值计数的毫秒时长类:

std::chrono::duration<short, std::ratio<1, 1000>>(1毫秒是1/1000秒)

std::chrono::milliseconds ms(22222);

std::chrono::seconds s = std::duration_cast<std::chrono::seconds>(ms);

3.3 时间点类

由类模板std::chrono::time_point<>的实例表示,第一个参数指明所参考的时钟,第二个参数指明计时单元。

例如:以系统时钟为参考,计时单元为分钟

std::chrono::time_point<std::chrono::system_clock, std::chrono::minutes>

可以将时间点加减时长,从而得出新的时间点。

比如:

std::chrono::system_clock::now() + std::chrono::minutes(423)

如果两个时间点共享一个时钟,我们也可以用它相减来得到时长。

3.4 接受超时时限的函数

用来设定休眠时间,延迟线程执行,设置超时时长等。

4 运用同步操作简化代码

5 小结

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

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

相关文章

Outlook2024版如何回到经典Outlook

Outlook2024版如何回到经典Outlook 如果新加入一家公司&#xff0c;拿到的电脑&#xff0c;大概率是最新版的Windows, 一切都是新的。 如果不coding, 使用国产的foxmail大概就可以解决一切问题了。可惜老程序员很多Coding都是基于传统Outlook的&#xff0c;科技公司所有人都是I…

【大模型】7 天 AI 大模型学习

因为想先快速把 llama 模型学习了&#xff0c;所以跳了两次课&#xff0c;这是这两次课的主要内容&#xff0c;后面有时间会补充上的 &#xff5e; 主要内容有&#xff1a;一些微调技术&#xff08;Alpaca、AdaLoRA、QLoRA&#xff09;、Prefix Tuning、Quantization 1. Alpaca…

网关如何识别和阻止网络攻击

网关在识别和阻止网络攻击方面扮演着关键角色&#xff0c;它通过多种技术和机制来确保网络的安全。以下是网关如何识别和阻止网络攻击的一些主要方法&#xff1a; 1.深度包检测&#xff08;DPI&#xff09; 网关可以对经过的数据包进行深度分析&#xff0c;检查数据包的头部、负…

解决 ffmpeg “Unknown encoder ‘hevc_nvenc‘“

目录 项目场景: 问题描述 原因分析: 解决方案: 项目场景: ffmpeg 剪切视频 问题描述 详细报错: [vost#0:0 @ 0x46ae00] Unknown encoder hevc_nvenc 原因分析: ffmpeg 安装错误 解决方案: 重新安装ffmpeg: conda install ffmpeg 检查当前安装的 FFmpeg 是否支…

# 设计一个 ATM 机器(LeetCode 第2241题)

设计一个 ATM 机器&#xff08;LeetCode 第2241题&#xff09; 在本篇博客中&#xff0c;我们将深入探讨如何设计一个 ATM 机器&#xff0c;以满足存取钞票的需求。这个问题来源于 LeetCode 的第2241题&#xff0c;旨在考察如何高效地管理不同面额的钞票并处理用户的存取请求。…

操作系统复习(理论版)

目录 只会在选择填空出现类型 第一章&#xff1a;操作系统导论 操作系统介绍 不得不知道的概念 可能出现在答题的类型 第二章&#xff1a;进程调度 进程管理&#xff1a; 处理机调度&#xff1a; 进程同步&#xff1a; 死锁&#xff1a; 预防死锁&#xff1a; 避免死…

概述(讲讲python基本语法和第三方库)

我是北子&#xff0c;这是我自己写的python教程&#xff0c;主要是记录自己的学习成果方便自己日后复习&#xff0c; 我先学了C/C&#xff0c;所以这套教程中可能会将很多概念和C/C去对比&#xff0c;所以该教程大概不适合零基础的人。 it seems that python nowadays 只在人工…

Linux(Centos 7.6)命令详解:ls

1.命令作用 列出目录内容(list directory contents) 2.命令语法 Usage: ls [OPTION]... [FILE]... 3.参数详解 OPTION: -l&#xff0c;long list 使用长列表格式-a&#xff0c;all 不忽略.开头的条目&#xff08;打印所有条目&#xff0c;包括.开头的隐藏条目&#xff09…

详解MySQL SQL删除(超详,7K,含实例与分析)

文章目录 前言1. 删除表中的所有记录基本语法使用场景注意事项运用实例分析说明2. 删除特定记录基本语法使用场景注意事项运用实例分析说明3. 删除单条记录基本语法使用场景注意事项运用实例分析说明4. 删除违反引用完整性的记录基本语法使用场景注意事项运用实例分析说明5. 删…

[图形渲染] 【Unity】UnityShader操作基础6-OpenGL与DirectX中的纹理坐标差异及语法差异分析

在现代图形编程中,OpenGL和DirectX作为两大主流图形API,各自有其独特的处理方式和语法规范。尽管它们的功能和目的相似,但在某些细节上却存在显著差异,尤其是在纹理坐标和着色器编程的相关操作上。本文将探讨OpenGL与DirectX在纹理坐标、语法以及语义上的差异,并给出一些跨…

numpy数组学习

前言 有时看到数组&#xff0c;容易被绕晕&#xff0c;不知道每个列表的数字分别代表什么&#xff0c;因此记录下来&#xff0c;用于后续参考。 1.轮廓点坐标 (n, 1, 2) 格式解释 (n, 1, 2)&#xff1a; n&#xff1a;表示轮廓上的点数。每个点都是轮廓的一部分。 1&#x…

改善 Kibana 中的 ES|QL 编辑器体验

作者&#xff1a;来自 Elastic Marco Liberati 随着新的 ES|QL 语言正式发布&#xff0c;Kibana 中开发了一种新的编辑器体验&#xff0c;以帮助用户编写更快、更好的查询。实时验证、改进的自动完成和快速修复等功能将简化 ES|QL 体验。 我们将介绍改进 Kibana 中 ES|QL 编辑器…

Mono里运行C#脚本25—mono_codegen

前面分析怎么样找到主函数Main的入口点功能,也就是说已经找到了这个函数的CIL代码。虽然找到了代码,但是还不能执行它的,因为它是一种虚拟机的代码。也就是说它是假的代码,不是现实世界存在的机器的代码,因此不能直接执行,必须经过后端编译器的再次编译才能真正运行它。下…

基于Spring Boot的紧急物资管理系统

基于Spring Boot的紧急物资管理系统是一个非常实用的应用&#xff0c;特别是在应对自然灾害、公共卫生事件等情况下。该系统可以帮助管理者有效地追踪和分配物资&#xff0c;确保资源能够及时到达需要的地方。以下是一个基本的实现思路和一些关键组件&#xff1a; 项目规划 需…

mini-dog-c编译器之词法分析

mini-dog-c 是一个小型的 C 语言编译器&#xff0c;是我学习和理解编译器基本工作原理的实践项目。其词法分析器能够识别 C 语言的基本语法元素&#xff0c;包括常见的标识符、整数和浮点数字面量、布尔字面量以及字符串字面量。此外&#xff0c;它还支持基本的算术和逻辑操作符…

机器学习基础-概率图模型

&#xff08;一阶&#xff09;马尔科夫模型的基本概念 状态、状态转换概率、初始概率 状态转移矩阵的基本概念 隐马尔可夫模型&#xff08;HMM&#xff09;的基本概念 条件随机场&#xff08;CRF&#xff09;的基本概念 实际应用中的马尔科夫性 自然语言处理&#xff1a; 在词…

力扣hot100——链表

160. 相交链表 class Solution { public:ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {set<ListNode*> s;ListNode* h headA;while (h ! NULL) {s.insert(h);h h->next;}h headB;while (h ! NULL){if (s.find(h) ! s.end()) {return h;}h …

Qt打包为exe文件

个人学习笔记 选择release 进入项目文件夹&#xff0c;查看releas生成的文件 releas文件路径 进入release看到exe文件&#xff0c;但是无法执行 将exe文件单独放到一个文件夹内 选择MinGW 用CD 进入存放exe文件的路径&#xff0c;输入下面指令 cd J:\C\Qt\test4-3-1 windeploy…

VScode怎么重启

原文链接&#xff1a;【vscode】vscode重新启动 键盘按下 Ctrl Shift p 打开命令行&#xff0c;如下图&#xff1a; 输入Reload Window&#xff0c;如下图&#xff1a;

小屏幕下通过css自动实现上下位置颠倒例子

<!DOCTYPE html> <html><head><meta charset"utf-8"><meta name"viewport" content"widthdevice-width, initial-scale1"><title>Demo</title><!-- 请勿在项目正式环境中引用该 layui.css 地址 --…