libhv教程05--事件循环以及定时器的简单使用
事件循环简介很多同学不理解事件循环的概念,所以这里有必要前置说明一下。对于大多数长时间运行程序来说,都会有主循环的存在。如窗口界面程序,就是等待键盘、鼠标等外设的输入,界面做出相应的变化。典型的如windows窗口消息机制// windows窗口消息循环MSG msg;while (GetMessage(&msg, NULL, 0, 0)) {TranslateMessage(&m
事件循环简介
很多同学不理解事件循环的概念,所以这里有必要前置说明一下。
对于大多数长时间运行程序来说,都会有主循环的存在。
如窗口界面程序,就是等待键盘、鼠标等外设的输入,界面做出相应的变化。
我们以windows窗口消息机制
举例说明:
// windows窗口消息循环
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
此循环所在的线程我们称之为GUI线程
(即窗口所在线程),MFC、WPF、Qt
等界面框架不过是将此过程给封装了。
理解了窗口消息循环的存在,其实就不难理解windows下老生常谈的问题:SendMessage
与PostMessage
的区别。
SendMessage
和PostMessage
都是windows提供的用来向窗口线程发送消息的API。
不同之处是SendMessage
是同步的,如果SendMessage
调用线程和窗口线程位于同一线程,则直接调用窗口过程处理此消息;如果不是同一线程,则会阻塞等待窗口线程处理完此消息再返回。
PostMessage
是异步的,将消息投递到窗口消息队列中就返回了,所以使用PostMessage
传递参数时需要注意不能使用栈上的局部变量。
IO多路复用简介
GUI线程具有主循环,网络IO线程亦是如此。
我们都知道IO可分为阻塞BIO
与非阻塞NIO
。
libhv的头文件hsocket.h中提供了跨平台的设置阻塞与非阻塞的方法:
#ifdef OS_WIN
static inline int blocking(int sockfd) {
unsigned long nb = 0;
return ioctlsocket(sockfd, FIONBIO, &nb);
}
static inline int nonblocking(int sockfd) {
unsigned long nb = 1;
return ioctlsocket(sockfd, FIONBIO, &nb);
}
#else
#define blocking(s) fcntl(s, F_SETFL, fcntl(s, F_GETFL) & ~O_NONBLOCK)
#define nonblocking(s) fcntl(s, F_SETFL, fcntl(s, F_GETFL) | O_NONBLOCK)
#endif
对于BIO,伪代码如下:
while (1) {
readbytes = read(fd, buf, len);
if (readbytes <= 0) {
close(fd);
break;
}
...
writebytes = write(fd, buf, len);
if (writebytes <= 0) {
close(fd);
break;
}
}
因为读写都是阻塞的,所以一个IO线程只能处理一个fd,对于客户端尚可接受,对于服务端来说,每accept
一个连接,就创建一个IO线程去读写这个套接字,并发达到几千就需要创建几千个线程,线程上下文的切换开销都会把系统占满。
所以IO多路复用机制应运而生,如最早期的select
、后来的poll
,linux
的epoll
、windows
的iocp
、bsd
的kqueue
、solaris
的port
等,都属于IO多路复用机制。非阻塞NIO搭配IO多路复用机制就是高并发的钥匙
。
关于select、poll、epoll
的区别,可自行百度,这里就不展开说了。仅以select为例,写下伪代码:
while (1) {
int nselect = select(max_fd+1, &readfds, &writefds, &exceptfds, timeout);
if (nselect == 0) continue;
for (int fd = 0; fd <= max_fd; ++fd) {
// 可读
if (FD_ISSET(fd, &readfds)) {
...
read(fd, buf, len);
}
// 可写
if (FD_ISSET(fd, &writefds)) {
...
write(fd, buf, len);
}
}
}
通过IO多路复用机制,一个IO线程就可以同时监听多个fd了,以现代计算机的性能,一个IO线程即可处理几十万数量级别的IO读写。
libhv下的event模块正是封装了多种平台的IO多路复用机制,提供了统一的事件接口
,是libhv的核心模块。
libhv中的事件包括IO事件
、timer定时器事件
、idle空闲事件
、自定义事件
(见hloop_post_event
接口,作用类似于windows窗口消息机制的PostMessage
)。
源码分析推荐群友qu1993的博客
使用libhv创建一个事件循环
c版本
#include "hv/hloop.h"
// 定时器回调函数
static void on_timer(htimer_t* timer) {
printf("time=%lus\n", (unsigned long)time(NULL));
}
int main() {
// 新建一个事件循环结构体
hloop_t* loop = hloop_new(0);
// 添加一个定时器
htimer_add(loop, on_timer, 1000, INFINITE);
// 运行事件循环
hloop_run(loop);
// 释放事件循环结构体
hloop_free(&loop);
return 0;
}
事件循环测试代码examples/hloop_test.c
定时器测试代码见examples/htimer_test.c
c++版本
#include "hv/EventLoop.h"
using namespace hv;
int main() {
// 新建一个事件循环对象
EventLoopPtr loop(new EventLoop);
// 设置一个定时器
loop->setInterval(1000, [](TimerID timerID){
printf("time=%lus\n", (unsigned long)time(NULL));
});
// 运行事件循环
loop->run();
return 0;
}
事件循环测试代码evpp/EventLoop_test.cpp
定时器测试代码见evpp/TimerThread_test.cpp
Tips:
上述示例里新建了一个EventLoop
事件循环对象来起定时器,实际使用中因为TcpServer、TcpClient、UdpServer、UdpClient、HttpServer、WebSocketServer、WebSocketClient
等底层都是基于EventLoop
的,在它们的事件回调里就可以直接调用setInterval/setTimeout
来起定时器,和javascript
一样的便利,当然你也可以通过currentThreadEventLoop
宏获取到当前所在线程的事件循环对象,来做更多的事情。
示例可参考 evpp/TcpClient_test.cpp,就是在onConnection
连接回调里直接调用setInterval
定时发送消息。
evpp模块被设计成只包含头文件,不参与编译。 hloop.h中的c接口被封装成了c++的类,参考了muduo和evpp。
类设计如下:
├── Buffer.h 缓存类
├── Channel.h 通道类,封装了hio_t
├── Event.h 事件类,封装了hevent_t、htimer_t
├── EventLoop.h 事件循环类,封装了hloop_t
├── EventLoopThread.h 事件循环线程类,组合了EventLoop和thread
├── EventLoopThreadPool.h 事件循环线程池类,组合了EventLoop和ThreadPool
├── TcpClient.h TCP客户端类
├── TcpServer.h TCP服务端类
├── UdpClient.h UDP客户端类
└── UdpServer.h UDP服务端类
示例代码位于evpp目录下
Tips:
EventLoop
中实现了muduo
有的两个接口,runInLoop
和queueInLoop
,我觉得命名不错,也直接采用了。runInLoop
对应SendMessage
,queueInLoop
对应PostMessage
,这么解释大家是不是更理解文章开头的铺垫了;EventLoopThreadPool
的核心思想就是one loop per thread
;
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)