基于事件驱动的并发

基于事件驱动的并发模式出现在 GUI 应用程序中,还有某些服务器编程中,比如说 node-js,但它们的根基还在基于 Unix 系统提供的底层功能构建起来的。

多线程编程有两个问题,第一个是编写正确的多线程程序是个很有挑战的事情,第二个是多线程环境下编程者没有权力控制线程的调度、线程的调度依赖底层 OS。这可能并不能如编程者所愿。

最基本的 Idea:事件循环

The approach is quite simple: you simply wait for something (i.e., an “event”) to occur;
when it does, you check what type of event it is and do the small amount of work it requires
That’s it!

我们只需等待事件的发生,然后触发执行我们预定义好的代码,就可以了。

伪代码:

while (1) {
    events = getEvents();
    for (e in events)
        processEvent(e);
}

循环内通过 getEvents() 函数等待某个时间返回,对于每个返回的事件,一次处理一个,处理事件的函数也叫做 event handler。重要的是,用户能决定哪个 event hanlder 去处理事件这个动作,其实等价于调度了。

重要的 API:select() or poll()

首先,我们要能收到事件。大多数操作系统中,可以通过两个系统调用获得 select()poll()。这两个接口提供的功能都是检查是否有感兴趣的 I/O 事件进来,比如说一个 Web Server 希望知道当前是否有网络包进来。

int select(int nfds,
            fd_set * restrict_readfds,
            fd_set * restric_writefds,
            fd_set * restrict_errorfds,
            struct timeval * restrict_timeout);

下面的描述来自于 man page:

select() examines the I/O descriptor sets whose addresses are passed in readfds, writefds, and errorfds to see if some of their descriptors are ready for reading, are ready for writing, or have an exceptional condition pending, respectively. The first nfds descriptors are checked in each set, i.e., the descriptors from 0 through nfds-1 in the descriptor sets are examined. On return, select() replaces the given descriptor sets with subsets consisting of those descriptors that are ready for the requested operation. select() returns the total number of ready descriptors in all the sets.

类似的

poll() 系统调用也是类似的。

这里其实会扯到一些概念:阻塞(blocking)和非阻塞(non-blocking)接口。

  • Blocking(或者叫 synchronous)接口是指接口必须完成他们的工作才能将控制流返回给调用者
  • Non-blocking(或者叫 asynchronous)接口是指调用之后就去做一些事情,但是不一定完成就可以返回给调用者,接口要完成的任务可以在别处继续运行

比如说通常 I/O 操作是很慢的,程序可以发出 I/O 操作之后就返回去处理别的事情,等到 I/O 完成或者失败了再回来处理,而不用一直浪费等待那段时间。

这下清楚了,这本教材还是比较权威的,不用再听网上那些人扯什么阻塞非阻塞的错误概念的。

为什么更简单了?因为没有锁了

因此一次只有一个事件被处理,因此不需要加锁。

虽然书上是这么说的,但是不严谨,因为如果事件的处理者其实是在一个线程当中的,而它要去修改某些可见范围更加大的变量的话,其实还是需要锁的。

问题:阻塞的系统调用

当 event hanlder 遇到了一个耗时很长的阻塞系统调用会怎么样?

会被长时间阻塞。这是非常影响性能的

一个解决办法:异步 I/O

通常来说,I/O 操作是比较耗时的,比如说 open()read() 之类的

另外一个问题,状态管理

尝试编写过状态机的朋友应该深有体会。

基于事件驱动的困难还有哪些?

  • CPU 多核是否能有效利用到的问题,尽管使用上了事件驱动,但是其实还是很难避开 critical section 的问题
  • 不是所有场景都适合事件驱动,有些场景根本就不能利用到事件驱动,比如说分页
  • 状态的流转是复杂的,得要注意管理
  • 异步 I/O 可能已经在磁盘类接口有,但是网络方面的可能还没有(这个我没调研过),通常是结合 select() 来处理网络 I/O, aio 库处理磁盘 I/O

总结

感觉这节介绍的比较简单,草草略过,基于事件驱动开发出来的开源软件还是蛮多的,比如说著名的 Nginx 就值得去研究。

引用

《Event-based Concurrency》

Comments
Write a Comment