C++ 简易消息循环
前言
本文将向大家介绍如何使用 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 个消息。
瓶颈
效率瓶颈主要在以下几的地方:
- 锁粒度太高。每次投递消息与取出消息都会锁住整个循环
- 消息多了之后,
priority_queue插入、移除的耗时变得可观
优化方向
针对上述原因,可以采取以下优化措施:
- 减小锁粒度或者采用无锁数据结构(参考 Disruptor 的 RingBuffer)
- 消息一般可分为两类:一类是定时消息,要在某个时间点执行;另一类是非定时消息,只要执行它就行。因此可以把消息队列分为至少两个:一个先入先出队列;一个带排序的队列(堆)
- 采用两个缓冲区。一个用于写,一个用于读
- 采用对象池优化内存分配
优化过程要注意以下问题:
- 消息的回调函数内可能会再调用
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++ 简易消息循环的更多相关文章
- Android的消息循环机制 Looper Handler类分析
Android的消息循环机制 Looper Handler类分析 Looper类说明 Looper 类用来为一个线程跑一个消息循环. 线程在默认情况下是没有消息循环与之关联的,Thread类在ru ...
- [转]Handler MessageQueue Looper消息循环原理分析
Handler MessageQueue Looper消息循环原理分析 Handler概述 Handler在Android开发中非常重要,最常见的使用场景就是在子线程需要更新UI,用Handler ...
- QObject::deleteLater()并没有将对象立即销毁,而是向主消息循环发送了一个event,下一次主消息循环收到这个event之后才会销毁对象 good
程序编译运行过程很顺利,测试的时候也没发现什么问题.但后来我随手上传了一个1G大小的文件,发现每次文件上传到70%左右的时候程序就崩溃了,小文件就没这个问题.急忙打开任务管理器,这才发现上传文件的时候 ...
- ios - 图片自动轮播定时器(NSTimer)以及消息循环模式简介
本文只是演示如何设置图片轮播的定时器. 创建全局变量NSTimer 程序启动后就开始轮播图片,所以在- (void)viewDidLoad中就启动定时器. 将定时器放入消息循环池中.- (void)v ...
- WinMain初始化详细过程以及消息循环
主要内容:详细介绍WinMain函数的初始化过程以及消息循环 1.窗口类定义 通过给窗口类数据结构WNDCLASS赋值完成, 该数据结构中包含窗口类的各种属性 <1>LoadIcon 作用 ...
- 【转】Android开发实践:自定义带消息循环(Looper)的工作线程
http://ticktick.blog.51cto.com/823160/1565272 上一篇文章提到了Android系统的UI线程是一种带消息循环(Looper)机制的线程,同时Android也 ...
- TMsgThread, TCommThread -- 在delphi线程中实现消息循环
http://delphi.cjcsoft.net//viewthread.php?tid=635 在delphi线程中实现消息循环 在delphi线程中实现消息循环 Delphi的TThread类使 ...
- QT源码解析(一) QT创建窗口程序、消息循环和WinMain函数
QT源码解析(一) QT创建窗口程序.消息循环和WinMain函数 分类: QT2009-10-28 13:33 17695人阅读 评论(13) 收藏 举报 qtapplicationwindowse ...
- Chrome中的消息循环
主要是自己做个学习笔记吧,我经验也不是很丰富,以前学习多线程的时候就感觉写多线程程序很麻烦.主要是线程之间要通信,要切线程,要同步,各种麻烦.我本身的工作经历决定了也没有太多的工作经验,所以chrom ...
- Thread+Handler 线程 消息循环(转载)
近来找了一些关于android线程间通信的资料,整理学习了一下,并制作了一个简单的例子. andriod提供了 Handler 和 Looper 来满足线程间的通信.例如一个子线程从网络上下载了一副图 ...
随机推荐
- Apache DolphinScheduler(2.x和3.x版本) 本地环境搭建教程一览
在迅速变化的技术领域,本地环境的搭建和调试对于软件开发的效率和效果至关重要.本文将详细介绍如何为Apache DolphinScheduler搭建一个高效的本地开发环境,包括2.x和3.x版本的设置方 ...
- Meissel_Lehmer模板
复杂度 \(O(n^\frac 23)\),计算 \(1\sim n\) 的素数个数 #define div(a, b) (1.0 * (a) / (b)) #define half(x) (((x) ...
- JWT(JSON WEB TOKEN)是玩具吗
JWT当然不是玩具,理解其设计意图,和适用场景自然会发现存在的就是有价值的 JWT: JSON Web Token 起源和定义 JWT(JSON Web Token)是由 IETF(Internet ...
- Kubernetes-3:使用kubeadm部署k8s环境及常见报错解决方法
k8s集群安装 环境说明: k8s-Master-Centos8 ip:192.168.152.53 k8s-Node1-Centos7 ip:192.168.152.253 k8s-Node2-Ce ...
- Pandas从入门到放弃
公众号本文地址:https://mp.weixin.qq.com/s/mSkA5KvL1390Js8_1ZBiyw Pandas简介 Pandas是Panel data(面板数据)和Data anal ...
- Redis 入门 - C#|.NET Core客户端库六种选择
经过前面的Redis基础学习,今天正式进入编码阶段了,进入编码阶段我们又同样面临一道多选题,选择什么客户端库?要是有选择困难症的又要头疼了.不过别担心我先头疼,今天就给大家介绍6款.NET系Redis ...
- 知乎问题:为什么很多web项目还是使用 px,而不是 rem?
阅读过几篇关于 px rem 的文章,感觉 rem 很强大.但是自己接触到的公司项目全部都使用 px,想知道为什么.是我司技术更新落后了吗? 我们当然有在用 vw 和 vh,但是只是在 layout ...
- TypeScript – 冷知识
当 generic return 遇上 parameter 报错了.原因是 querySelector 默认返回类型是抽象的 Element. 而 method 参数要求的是具体的 InputElem ...
- JAVAEE——tomcat安装
一.下载 1.网址:Tomcat官网 选择版本:点击左边Download下的which version,根据Supported Java Versions来选择合适的Tomcat版本 下载软件:点击左 ...
- QT数据可视化框架编程实战之三维散点图从入门到精通_补天云QT技术培训专家
简介 本文将介绍支持GPU图形硬件加速的QT数据可视化框架编程实战之三维散点图从入门到精通_补天云QT技术培训专家.本文将创建一个含有三个序列的三维散点图,同时设定了坐标轴的标题和标签,使得用户点击表 ...