事件循环简介

很多同学不理解事件循环的概念,所以这里有必要前置说明一下。
对于大多数长时间运行程序来说,都会有主循环的存在。

如窗口界面程序,就是等待键盘、鼠标等外设的输入,界面做出相应的变化。
我们以windows窗口消息机制举例说明:

// windows窗口消息循环
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
	TranslateMessage(&msg);
	DispatchMessage(&msg);
}

此循环所在的线程我们称之为GUI线程(即窗口所在线程),MFC、WPF、Qt等界面框架不过是将此过程给封装了。

理解了窗口消息循环的存在,其实就不难理解windows下老生常谈的问题:SendMessagePostMessage的区别。
SendMessagePostMessage都是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、后来的polllinuxepollwindowsiocpbsdkqueuesolarisport等,都属于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有的两个接口,runInLoopqueueInLoop,我觉得命名不错,也直接采用了。runInLoop对应SendMessagequeueInLoop对应PostMessage,这么解释大家是不是更理解文章开头的铺垫了;
  • EventLoopThreadPool的核心思想就是one loop per thread;
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐