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.rhkb.cn/news/502330.html

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

相关文章

PWN 的知识之如何利用栈溢出利用后门函数

PWN 的知识之如何利用栈溢出利用后门函数 利用栈溢出漏洞调用原本存在的后门函数&#xff08;例如 get_flag 或system("/bin/sh")&#xff09;是二进制漏洞利用中的一种常见技术,相信各位网安的师傅或多或少都听说过&#xff0c;那么如何利用栈溢出来利用后门函数呢…

基于YOLO11的道路缺陷检测系统

基于YOLO11的道路缺陷检测系统 (价格90) 包含 [cracks, potholes] [裂缝, 凹坑] 2个类 通过PYQT构建UI界面&#xff0c;包含图片检测&#xff0c;视频检测&#xff0c;摄像头实时检测。 &#xff08;该系统可以根据数据训练出的yolo11的权重文件&#xff0c;运用在其他…

JAVA:Spring Boot 集成 Quartz 实现分布式任务的技术指南

1、简述 Quartz 是一个强大的任务调度框架&#xff0c;允许开发者在应用程序中定义和执行定时任务。在 Spring Boot 中集成 Quartz&#xff0c;可以轻松实现任务的调度、管理、暂停和恢复等功能。在分布式系统中&#xff0c;Quartz 也支持集群化的任务调度&#xff0c;确保任务…

数据分析-Excel

数据类型和函数初步 Excel中有文本类型和数值类型–但是无法用肉眼分辨出来isnumber来区分是否是数值类型text和value函数可以完成数值类型以及文本类型的转换单元格第一位输入’方式明确输入的是文本sum函数必须是数值类型 文本连接-and-or-not-if-mod-max函数 字符串的连接…

深入了解 SSL/TLS 协议及其工作原理

深入了解 SSL/TLS 协议及其工作原理 一. 什么是 SSL/TLS?二. SSL/TLS 握手过程三. SSL/TLS 数据加密与传输四. 总结 点个免费的赞和关注&#xff0c;有错误的地方请指出&#xff0c;看个人主页有惊喜。 作者&#xff1a;神的孩子都在歌唱 一. 什么是 SSL/TLS? 安全套接层&am…

【NLP高频面题 - Transformer篇】Transformer的输入中为什么要添加位置编码?

Transformer的输入中为什么要添加位置编码&#xff1f; 重要性&#xff1a;★★★ Transformer 将句子中的所有词并行地输入到神经网络中。并行输入有助于缩短训练时间&#xff0c;同时有利于学习长期依赖。不过&#xff0c;并行地将词送入 Transformer&#xff0c;却不保留词…

【Unity3D】UGUI Canvas画布渲染流程

目录 Screen Space - Overlay Screen Space - Camera World Space UI合批分析&#xff08;建议不看 直接看FrameDebugger测试&#xff09; 优化UI合批 1、Image图片纹理不同导致合批失败 2、文本和图片相交以及排序对合批的影响 参考文档&#xff1a;画布 - Unity 手册…

计算机的错误计算(二百零一)

摘要 用两个大模型计算 &#xff0c;结果保留 10位有效数字。实验表明&#xff0c;两个大模型的输出均只有1位正确数字&#xff1b;并它们几乎相同&#xff1a;仅最后1位数字不同。 例1. 计算 , 结果保留 10位有效数字。 下面是与一个数学解题器的对话。 以上为与一个数学解…

完全分布式部署Hadoop集群

(1)第一步&#xff1a;安装Hadoop&#xff0c;使用如下命令&#xff1a; tar -zvxf /export/software/Hadoop-3.3.4.tar.gz -C /export/servers (2)第二步&#xff1a;配置Hadoop系统环境变量 在liumengting1上执行vi /etc/profile命令配置系统环境变量profile&#xff0c;在…

Redis数据库笔记—— Hash(哈希)的扩容机制(rehash)

大家好&#xff0c;这里是Good Note&#xff0c;关注 公主号&#xff1a;Goodnote&#xff0c;专栏文章私信限时Free。详细介绍Hash&#xff08;哈希&#xff09;的扩容机制(rehash)、源码、以及扩容和缩容过程。 文章目录 Redis 字典&#xff08;dict&#xff09;结构源码哈希…

使用命令行管理git项目

# 初始化一个新的Git仓库 git init # 添加文件到暂存区 git add <file> # 提交暂存区的更改到仓库 git commit -m "commit message" # 查看当前仓库的状态 git status # 查看提交历史 git log # 查看文件的改动 git diff <file> # 创建一个新…

设计模式 创建型 工厂模式(Factory Pattern)与 常见技术框架应用 解析

工厂模式&#xff08;Factory Pattern&#xff09;是一种创建型设计模式&#xff0c;它提供了一种封装对象创建过程的方式&#xff0c;使得对象的创建与使用分离&#xff0c;从而提高了系统的可扩展性和可维护性。 一、核心思想 工厂模式的核心思想是将“实例化对象”的操作与…

【Block总结】Conv2Former中的Block,卷积调制块,简化了自注意力机制,提高了内存效率

论文介绍 论文链接&#xff1a;https://arxiv.org/pdf/2211.11943 研究背景&#xff1a;论文指出&#xff0c;尽管当前研究者们通过利用大核卷积、高阶空间交互或稀疏卷积核等方法对卷积神经网络&#xff08;ConvNets&#xff09;的设计进行了重新思考&#xff0c;但如何更有…

w139华强北商城二手手机管理系统

&#x1f64a;作者简介&#xff1a;多年一线开发工作经验&#xff0c;原创团队&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的网站项目。 代码可以查看文章末尾⬇️联系方式获取&#xff0c;记得注明来意哦~&#x1f339;赠送计算机毕业设计600个选题excel文…

ThreadPoolExecutor keepAliveTime 含义

现象 在线上环境排查问题时&#xff0c;某个线程池在某个时间点新建线程达到设定的最大线程数 maximumPoolSize&#xff0c;后续流量降低后当前线程数仍未回落&#xff0c;仍然为最大线程数&#xff0c;阻塞队列中有任务&#xff0c;但是活跃线程数显著减少。 之前的认知 固…

如何恢复已删除的 Telegram 消息 [iOSamp;Android]

Telegram 是一款功能强大的消息应用程序&#xff0c;因其易用性、隐私保护和众多炫酷功能而深受用户喜爱。然而&#xff0c;有时我们会不小心删除重要的消息。在这种情况下你应该做什么&#xff1f; 本文将为您提供简单有效的解决方案来恢复 Telegram 上已删除的消息&#xff…

Outlook2024版如何回到经典Outlook

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

动态库dll与静态库lib编程4:MFC规则DLL讲解

文章目录 前言一、说明二、具体实现2.1新建项目2.2 模块切换的演示 总结 前言 动态库dll与静态库lib编程4&#xff1a;MFC规则DLL讲解。 一、说明 1.前面介绍的均为Win32DLL&#xff0c;即不使用MFC的DLL。 2.MFC规则DLL的特点&#xff1a;DLL内部可以使用MFC类库、可以被其他…

若依中Feign调用的具体使用(若依微服务版自身已集成openfeign依赖,并在此基础上定义了自己的注解)

若依中Feign调用具体使用 注意&#xff1a;以下所有步骤实现的前提是需要在启动类上加入注解 EnableRyFeignClients 主要是为开启feign接口扫描 1.创建服务提供者(provider) 导入依赖(我在分析依赖时发现若依本身已经引入openfeign依赖,并在此基础上自定义了自己的EnableRyF…

CSS3——3. 书写格式二

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title></title></head><body><!--css书写&#xff1a;--><!--1. 属性名:属性值--><!--2.属性值是对属性的相关描述--><!--3.属性名必须是…