使用libevent这个库很长时间了,libevent虽然比较成熟,但由于这个库考虑到多线程的问题,里面的线程同步锁太多性能不是很好,同时问题也发现不少,尤其是在Linux下,时常莫名崩溃,很难找到原因。好在libuv现在已经很成熟了,经过使用发现性能非常优秀,有必要扩大使用。
libuv原来是为了Node.js而写的一个跨平台支撑库。它是围绕事件驱动的异步I/O模型设计的。
该库提供的不仅仅是针对不同I/O轮询机制的简单抽象:“handles”(句柄)和“streams”(流)为套接字和其他实体提供了高级抽象;还提供了跨平台文件I/O和多线程功能,还包括一些其它东西。
如下的图表说明组成libuv的不同部分以及它们与哪个子系统相关:
handles(句柄)和requests(请求)
libuv给用户提供了与event loop(事件循环)结合使用的两种抽象对象:handles(句柄) 与 requests(请求)。
handles 表示在活跃时能够执行某些操作的长寿命对象。例如:
1. 处于激活状态的prepare handle 在每次循环迭代中获得一次回调。
2. TCP 服务handle在每次新的连接到达时获得一个connection (连接)回调。
Requests 代表(通常)短期操作。这些操作可能是在一个handle对象上执行的:如write requests(写请求)用于在一个handle(句柄)上写数据;或者是独立的,如 getaddrinfo requests(请求)无须句柄可以直接在事件循环上运行。
事件循环是 libuv 的核心部分。它为所有的 I/O 操作建立了上下文,并且执行于一个单线程中。你可以在多个不同的线程中运行多个事件循环。除非另有说明,不然 libuv 的事件循环(以及其他循环或句柄提供的 API)并不是线程安全的。
事件循环遵循着普遍的单线程异步 I/O 行为:所有的(网络)I/O 体现在非阻塞的 socket 上,对于不同的平台,libuv 会选取最佳的轮询机制:Linux 上为 epoll ,OSX 和其他 BSD 上为 kqueue ,SunOS 上为 event ports , Windows 上则为 IOCP 。作为循环迭代的一部分,事件循环会阻塞并等待被添加的 socket 上 I/O 活动的发生。然后根据当前的 socket 情况(可读,可写,挂起)触发相应的回调函数。所以,一个句柄是可以执行读操作,写操作或其他 I/O 行为。
为了能更好的理解事件循环是如何工作的,下图展示了事件循环一次迭代的所有阶段:
-
事件循环中的“现在时间(now)”被更新。事件循环会在一次循环迭代开始的时候缓存下当时的时间,用于减少与时间相关的系统调用次数。
-
如果事件循环仍是存活(alive)的,那么迭代就会开始,否则循环会立刻退出。如果一个循环内包含激活的可引用句柄,激活的请求或正在关闭的句柄,那么则认为该循环是存活的。
-
执行到期的定时器(due timers)。所有在循环的“现在时间”之前到期的定时器都将在这个时候得到执行。
-
执行等待中的回调(pending callbacks)。正常情况下,所有的 I/O 回调都会在轮询 I/O 后立刻被调用。但是有些情况下,回调可能会被推迟至下一次循环迭代中再执行。任何上一次循环中被推迟的回调,都将在这个时候得到执行。
-
执行闲置句柄回调(idle handle callbacks)。尽管它有个不怎么好听的名字,但只要这些闲置句柄是激活的,那么在每次循环迭代中它们都会执行。
-
执行预备回调(prepare handle)。预备回调会在循环被 I/O 阻塞前调用。
-
开始计算轮询超时(poll timeout)。在为 I/O 阻塞前,事件循环会计算它即将会阻塞多长时间。以下为计算该超时的规则:
-
如果循环带着
UV_RUN_NOWAIT
标识执行,那么超时将会是 0 。 -
如果循环即将停止(
uv_stop()
已在之前被调用),那么超时将会是 0 。 -
如果循环内没有激活的句柄和请求,那么超时将会是 0 。
-
如果循环内有激活的闲置句柄,那么超时将会是 0 。
-
如果有正在等待被关闭的句柄,那么超时将会是 0 。
-
如果不符合以上所有,那么该超时将会是循环内所有定时器中最早的一个超时时间,如果没有任何一个激活的定时器,那么超时将会是无限长(infinity)。
-
-
事件循环为 I/O 阻塞。此时事件循环将会为 I/O 阻塞,持续时间为上一步中计算所得的超时时间。所有与 I/O 相关的句柄都将会监视一个指定的文件描述符,等待一个其上的读或写操作来激活它们的回调。
-
执行检查句柄回调(check handle callbacks)。在事件循环为 I/O 阻塞结束后,检查句柄的回调将会立刻执行。检查句柄本质上是预备句柄的对应物(counterpart)。
-
执行关闭回调(close callbacks)。如果一个句柄通过调用
uv_close()
被关闭,那么这将会调用关闭回调。 -
尽管在为 I/O 阻塞后可能并没有 I/O 回调被触发,但是仍有可能这时已经有一些定时器已经超时。若事件循环是以
UV_RUN_ONCE
标识执行,那么在这时这些超时的定时器的回调将会在此时得到执行。 -
迭代结束。如果循环以
UV_RUN_NOWAIT
或UV_RUN_ONCE
标识执行,迭代便会结束,并且uv_run()
将会返回。如果循环以UV_RUN_DEFAULT
标识执行,那么如果若它还是存活的,它就会开始下一次迭代,否则结束。
重要:虽然 libuv 的异步文件 I/O 操作是通过线程池实现的,但是网络 I/O 总是在单线程中执行的。
与网络 I/O 不同,并不存在 libuv 可以依靠的各特定平台下的文件 I/O 基础函数,所以目前的实现是在线程中执行阻塞的文件 I/O 操作来模拟异步。
更多关于跨平台异步文件 I/O 操作的内容,可参阅this post.
libuv 目前使用了一个全局的线程池,所有的循环都可以往其中加入任务。目前有三种操作会在这个线程池中执行:
-
文件系统操作
-
DNS 函数(getaddrinfo 和 getnameinfo)
-
通过
uv_queue_work()
添加的用户代码
注意:更多关于 libuv 线程池的信息请参阅此文。请牢记线程池的大小是有限的。