前言

本文将向大家介绍如何使用 C++ 的标准库实现一个异步和并发编程中都非常重要的编程模式:消息循环(Event Loop)。尽管市面上存在不少库也提供了同样的功能,但有时候出于一些原因,我们并不想引入外部库,就想要一个小巧、只使用 C++ 标准库的实现。

话不多说,上代码

using sys_clock = std::chrono::system_clock;

struct message {
sys_clock::time_point when;
std::function<void()> callback;
}; class message_loop {
public:
message_loop()
: _stop(false)
{
//
} message_loop(const message_loop&) = delete;
message_loop& operator=(const message_loop&) = delete; void run() {
while (!_stop) {
auto msg = wait_one();
msg.callback();
}
} void quit() {
post({sys_clock::now(), [this](){ _stop = true; } });
} void post(std::function<void()> callable) {
post({sys_clock::now(), std::move(callable)});
} void post(std::function<void()> callable, std::chrono::milliseconds delay) {
post({sys_clock::now() + delay, std::move(callable)});
} private:
struct msg_prio_comp {
inline bool operator() (const message& a, const message& b) {
return a.when > b.when;
}
}; using queue_type = std::priority_queue<message, std::vector<message>, msg_prio_comp>; std::mutex _mtx;
std::condition_variable _cv;
queue_type _msgs;
bool _stop; void post(message msg) {
{
auto lck = acquire_lock();
_msgs.emplace(std::move(msg));
}
_cv.notify_one();
} std::unique_lock<std::mutex> acquire_lock() {
return std::unique_lock<std::mutex>(_mtx);
} bool idle() const {
return _msgs.empty();
} const message& top() const {
return _msgs.top();
} message pop() {
auto msg = top();
_msgs.pop();
return msg;
} message wait_one() {
while (true) {
auto lck = acquire_lock();
if (idle())
_cv.wait(lck);
else if (top().when <= sys_clock::now())
return pop();
else {
_cv.wait_until(lck, top().when);
// 可能是新消息到达,再循环一次看看
}
}
}
};

接下来,演示一下使用方式:

int main() {
using namespace std;
using namespace std::chrono; message_loop *pLoop = nullptr;
thread th([&loop](){
message loop;
pLoop = &loop;
loop.run();
pLoop = nullptr;
}); logger() << "投递消息#1";
pLoop->post([](){
logger() << "消息#1 处理了";
}); logger() << "投递消息#2,延迟 500 毫秒";
pLoop->post([](){
logger() << "消息#2 处理了";
}, milliseconds(500)); logger() << "投递消息#3";
pLoop->post([](){
logger() << "消息#3 处理了";
}); logger() << "投递消息#4,延迟 1000 毫秒";
pLoop->post([](){
logger() << "消息#4 处理了";
}, milliseconds(1000)); this_thread::sleep_for(milliseconds(1500));
pLoop->quit();
logger() << "退出";
th.join();
return 0;
}

运行上面的示例可能看到如下输出:

[11:22:33.000] 投递消息#1
[11:22:33.000] 投递消息#2,延迟 500 毫秒
[11:22:33.000] 消息#1 处理了
[11:22:33.000] 投递消息#3
[11:22:33.000] 消息#3 处理了
[11:22:33.000] 投递消息#4,延迟 1000 毫秒
[11:22:33.501] 消息#2 处理了
[11:22:34.000] 消息#4 处理了
[11:22:34.502] 退出

可见,相比单纯的先进先出队列,这个消息循环支持延迟消息,可以用来做简单定时器,覆盖更多使用场景。

效率

当然,这么简单的消息循环,效率如何呢。在我的 i5 10500 上,针对 1048576 个消息的压测结果为每毫秒能处理约 2400 个消息。

瓶颈

效率瓶颈主要在以下几的地方:

  1. 锁粒度太高。每次投递消息与取出消息都会锁住整个循环
  2. 消息多了之后,priority_queue 插入、移除的耗时变得可观

优化方向

针对上述原因,可以采取以下优化措施:

  1. 减小锁粒度或者采用无锁数据结构(参考 Disruptor 的 RingBuffer)
  2. 消息一般可分为两类:一类是定时消息,要在某个时间点执行;另一类是非定时消息,只要执行它就行。因此可以把消息队列分为至少两个:一个先入先出队列;一个带排序的队列(堆)
  3. 采用两个缓冲区。一个用于写,一个用于读
  4. 采用对象池优化内存分配

优化过程要注意以下问题:

  1. 消息的回调函数内可能会再调用 post 发送消息,容易发生死锁。

在我电脑上的测试表明,即使不采用无锁数据结构,只把锁粒度减小,就能把效率翻倍。

拓展

如果觉得 post 函数使用太麻烦,也可以稍稍拓展一下。

execSync

使得我们可以像使用 GCD 一样,把函数调用委派到相应队列中:

logger() << pLoop->execSync([](int a, int b) { return a + b; }, 1, 2);

实现如下:

template<class Func, typename... Args>
auto execSync(Func&& fn, Args&& ...args) {
if (std::this_thread::get_id() == _tid) { // _tid 是新引入的成员变量,表示 message_loop 所在的线程的 ID
return std::invoke(std::forward<Func>(fn), std::forward<Args>(args)...);
} using return_type = std::invoke_result_t<Func, Args...>;
std::packaged_task<return_type(Args&&...)> task(std::forward<Func>(fn)); post([&](){ task(std::forward<Args>(args)...); }); return task.get_future().get();
}

execAsync

execSync 的异步版本,用于想自己处理异步结果的情形:

auto result = pLoop->execAsync([](int a, int b) { return a + b; }, 1, 2);
// ...
logger() << result.get();

实现如下:

template<class Func, typename... Args>
[[nodiscard]] auto execAsync(Func&& fn, Args&& ...args) {
using return_type = std::invoke_result_t<Func, Args...>;
using task_type = std::packaged_task<return_type()>; auto pTask = std::make_shared<task_type>(
std::bind(
std::forward<Func>(fn),
std::forward<Args>(args)...)); post([pTask](){ (*pTask)(); }); return pTask->get_future();
}

循环方式

其实,循环的方式多种多样,像我遇到的场景就采用了下面的循环:

while (!quit) {
bool onceMore = myLogic();
if (!onceMore) {
while (!quit && !otherCondition()) {
message msg = getNext();
msg.callback();
}
}
else if (hasNext()) {
message msg = getNext();
msg.callback();
}
}

这种循环的特点是,myLogic() 会尽可能多的执行,同时消息来了也能及时处理,适合一些实时性高的场合。正是因为循环的方式多样,封装好的 message_loop 往往需要提供各种 hook 点,比如空闲处理、进入等待前、唤醒后等等。不过,灵活性增加后,效率就会牺牲一点,这时可以考虑把消息队列和消息循环分开。

C++ 简易消息循环的更多相关文章

  1. Android的消息循环机制 Looper Handler类分析

    Android的消息循环机制 Looper Handler类分析 Looper类说明   Looper 类用来为一个线程跑一个消息循环. 线程在默认情况下是没有消息循环与之关联的,Thread类在ru ...

  2. [转]Handler MessageQueue Looper消息循环原理分析

    Handler MessageQueue Looper消息循环原理分析   Handler概述 Handler在Android开发中非常重要,最常见的使用场景就是在子线程需要更新UI,用Handler ...

  3. QObject::deleteLater()并没有将对象立即销毁,而是向主消息循环发送了一个event,下一次主消息循环收到这个event之后才会销毁对象 good

    程序编译运行过程很顺利,测试的时候也没发现什么问题.但后来我随手上传了一个1G大小的文件,发现每次文件上传到70%左右的时候程序就崩溃了,小文件就没这个问题.急忙打开任务管理器,这才发现上传文件的时候 ...

  4. ios - 图片自动轮播定时器(NSTimer)以及消息循环模式简介

    本文只是演示如何设置图片轮播的定时器. 创建全局变量NSTimer 程序启动后就开始轮播图片,所以在- (void)viewDidLoad中就启动定时器. 将定时器放入消息循环池中.- (void)v ...

  5. WinMain初始化详细过程以及消息循环

    主要内容:详细介绍WinMain函数的初始化过程以及消息循环 1.窗口类定义 通过给窗口类数据结构WNDCLASS赋值完成, 该数据结构中包含窗口类的各种属性 <1>LoadIcon 作用 ...

  6. 【转】Android开发实践:自定义带消息循环(Looper)的工作线程

    http://ticktick.blog.51cto.com/823160/1565272 上一篇文章提到了Android系统的UI线程是一种带消息循环(Looper)机制的线程,同时Android也 ...

  7. TMsgThread, TCommThread -- 在delphi线程中实现消息循环

    http://delphi.cjcsoft.net//viewthread.php?tid=635 在delphi线程中实现消息循环 在delphi线程中实现消息循环 Delphi的TThread类使 ...

  8. QT源码解析(一) QT创建窗口程序、消息循环和WinMain函数

    QT源码解析(一) QT创建窗口程序.消息循环和WinMain函数 分类: QT2009-10-28 13:33 17695人阅读 评论(13) 收藏 举报 qtapplicationwindowse ...

  9. Chrome中的消息循环

    主要是自己做个学习笔记吧,我经验也不是很丰富,以前学习多线程的时候就感觉写多线程程序很麻烦.主要是线程之间要通信,要切线程,要同步,各种麻烦.我本身的工作经历决定了也没有太多的工作经验,所以chrom ...

  10. Thread+Handler 线程 消息循环(转载)

    近来找了一些关于android线程间通信的资料,整理学习了一下,并制作了一个简单的例子. andriod提供了 Handler 和 Looper 来满足线程间的通信.例如一个子线程从网络上下载了一副图 ...

随机推荐

  1. vue之组件的简单使用

    1.背景 2.组件的简单使用 <!DOCTYPE html> <html lang="en"> <head> <meta charset= ...

  2. Spring Boot 中使用 JSON Schema 来校验复杂JSON数据

    JSON是我们编写API时候用于数据传递的常用格式,那么你是否知道JSON Schema呢? 在数据交换领域,JSON Schema 以其强大的标准化能力,为定义和规范 JSON 数据的结构与规则提供 ...

  3. Apache DolphinScheduler 3.1.8 版本发布,修复 SeaTunnel 相关 Bug

    近日,Apache DolphinScheduler 发布了 3.1.8 版本.此版本主要基于 3.1.7 版本进行了 bug 修复,共计修复 16 个 bug, 1 个 doc, 2 个 chore ...

  4. .NET 8 中利用 MediatR 实现高效消息传递

    前言 MediatR 是 .NET 下的一个实现消息传递的库,轻量级.简洁高效,用于实现进程内的消息传递机制.它基于中介者设计模式,支持请求/响应.命令.查询.通知和事件等多种消息传递模式.通过泛型支 ...

  5. Es java API官网手册

    官方地址: 一.TransportClient客户端 https://www.elastic.co/guide/en/elasticsearch/client/java-api/7.5/index.h ...

  6. Catlan--卡特兰数--组合数学

    卡特兰数 \(Catlan\) ·赘述 其实发现卡特兰数和之前不同的是,前面的是给你公式,让你去求具体的例子,然而卡特兰数这里是给你大量例子来给你证明和解释什么是卡特兰数. ·定义 对于卡特兰数来说, ...

  7. Java并发之原子变量及CAS算法-上篇

    Java并发之原子变量及CAS算法-上篇   概述 本文主要讲在Java并发编程的时候,如果保证变量的原子性,在JDK提供的类中式怎么保证变量原子性的呢?.对应Java中的包是:java.util.c ...

  8. vue+webpack工程中怎样在vue页面中引入第三方非标准的JS库或者方法

    方法一:异步加载第三方库 在我们的vue工程中新建如下路径:src/utils/index.js,在index.js中实现如下方法: export function loadScript(url) { ...

  9. LoRaWAN网关价格干穿地板了

    废话不多说,不要998,不要688,以太网版本只要398,4G版本只要498. 先上图片 1.1 产品特点 ◆ 高性能嵌入式硬件平台 ◆ 使用工业级 Cat.1 4G 模块 ◆ 宽压输入 DC 9-2 ...

  10. 贝壳找房携手 Flutter,为三亿家庭提供更好的居住服务 | Flutter 开发者故事

    贝壳找房是科技驱动的新居住服务平台,致力于在二手房.新房.租房以及装修等居住领域为三亿家庭提供全方位的品质居住服务.如此庞大的用户群体,自然也有着十分多样和复杂的使用场景和需求.以往使用原生开发模式时 ...