前言

本文将向大家介绍如何使用 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 文件流下载

    /**  * 文件转为文件流  * @param {file} file //文件  */ export function getFileBlob(file) {   var dataUrl   va ...

  2. Java——N以内累加求和

    2024/07/15 1.题目 2.错误 3.分析 4.答案 1.题目 2.错误 import java.util.Scanner; public class Main { public static ...

  3. 网站接入qq登录

    网站接入qq登录可以省去注册账号,增加网站的体验度.那么要实现我自己搭建的站点天天博客的qq单点登录需要以下几个步骤:1,申请appid和appkey 首先要去https://connect.qq.c ...

  4. installer 命令介绍

    installer 命令是 macOS 系统中用于在命令行界面下安装 .pkg 格式的安装包的工具.这个命令特别有用于自动化安装软件或者在没有图形用户界面的情况下进行软件安装,比如在远程管理或者终端会 ...

  5. maven 打包 pom build

    <dependencyManagement> <dependencies> <dependency> <groupId>org.springframew ...

  6. “java.lang.NoClassDefFoundError: javax/servlet/jsp/jstl/core/LoopTag”解决方法

    在运行jsp项目的时候出现了这个java.lang.NoClassDefFoundError: javax/servlet/jsp/jstl/core/LoopTag报错,一直尝试更换jar文件等也没 ...

  7. canvas实现图片标记

    前言 由于业务需求,需要有一个图片标记功能,其实就是对图片画框画线做标记,类似微信的图片编辑 但是需要存下标记图及其标记的具体数据,.功能其实很简单,但刚开始的时候也是费了一些功夫的.我将原项目中该功 ...

  8. 剖析 Redis List 消息队列的三种消费线程模型

    Redis 列表(List)是一种简单的字符串列表,它的底层实现是一个双向链表. 生产环境,很多公司都将 Redis 列表应用于轻量级消息队列 .这篇文章,我们聊聊如何使用 List 命令实现消息队列 ...

  9. OData – API Versioning

    前言 先看这 3 篇 ASP.NET Core – Web API Versioning ASP.NET Core – Swagger OpenAPI (Swashbuckle) ASP.NET Co ...

  10. Figma 学习笔记 – Auto Layout

    用途 Auto Layout 有点像 CSS 的 Flex, 它还带有 responsive 的概念.使用它以后可以替代掉不少 constraints 的写法. 用法 一个 parent 抱着多个 c ...