范围
Boost.Asio 是一个 C++ 库,最初专注于网络,但其异步 I/O 功能已扩展到其他资源。此外,作为 Boost 库的一部分,为了避免与其他 Boost 库重复,Boost.Asio 的范围略有缩小。例如,Boost.Asio 不提供线程抽象,因为 Boost.Thread 已经提供了一个。
另一方面,libuv 是一个为 Node.js 设计的平台层 C 库。它为 Windows 的 IOCP、macOS 的 kqueue 和 Linux 的 epoll 提供抽象。此外,它的范围似乎有所扩大,包含了线程、线程池和线程间通信等抽象和功能。
在核心功能上,每个库都提供事件循环和异步 I/O 功能。它们在一些基本功能上有重叠,例如定时器、套接字和异步操作。libuv 具有更广泛的范围,提供了额外的功能,例如线程和同步抽象、同步和异步文件系统操作、进程管理等。相比之下,Boost.Asio 的原始网络焦点更加明显,提供了更丰富的网络相关功能,例如 ICMP、SSL、同步阻塞和非阻塞操作,以及用于常见任务的高级操作,包括从流中读取直到接收到换行符。
功能列表
以下是一些主要功能的简要对比。由于使用 Boost.Asio 的开发人员通常可以访问其他 Boost 库,因此我选择考虑那些直接提供或实现起来很简单的其他 Boost 库。
| 功能 | libuv | Boost |
|---|---|---|
| 事件循环 | 是 | Asio |
| 线程池 | 是 | Asio + Threads |
| 线程 | ||
| 线程支持 | 是 | Threads |
| 同步 | 是 | Threads |
| 文件系统操作 | ||
| 同步操作 | 是 | FileSystem |
| 异步操作 | 是 | Asio + Filesystem |
| 定时器 | 是 | Asio |
| 散/聚集 I/O | 否 | Asio |
| 网络 | ||
| ICMP | 否 | Asio |
| DNS 解析 | 仅异步 | Asio |
| SSL | 否 | Asio |
| TCP | 仅异步 | Asio |
| UDP | 仅异步 | Asio |
| 信号 | ||
| 处理 | 是 | Asio |
| 发送 | 是 | 否 |
| 进程间通信 | ||
| UNIX 域套接字 | 是 | Asio |
| Windows 命名管道 | 是 | Asio |
| 进程管理 | ||
| 分离 | 是 | Process |
| I/O 管道 | 是 | Process |
| 生成 | 是 | Process |
| 系统查询 | ||
| CPU | 是 | 否 |
| 网络接口 | 是 | 否 |
| 串口 | 否 | 是 |
| TTY | 是 | 否 |
| 共享库加载 | 是 | Extension [2] |
- 散/聚集 I/O。
- Boost.Extension 从未提交给 Boost 审核。如这里所述,作者认为它已完成。
事件循环
虽然 libuv 和 Boost.Asio 都提供事件循环,但它们之间存在一些微妙的差异:
libuv 支持多个事件循环,但不支持从多个线程运行同一个循环。因此,在使用默认循环(uv_default_loop())时需要注意,而不是创建一个新循环(uv_loop_new()),因为另一个组件可能正在运行默认循环。Boost.Asio 没有默认循环的概念;所有 io_service 都是它们自己的循环,允许多个线程运行。为了支持这一点,Boost.Asio 在内部进行了锁定,但牺牲了一些性能。Boost.Asio 的修订历史显示,已经进行了几次性能改进以最小化锁定。
线程池
libuv 通过 uv_queue_work 提供线程池。线程池大小可以通过环境变量 UV_THREADPOOL_SIZE 配置。工作将在事件循环之外执行,在线程池中进行。一旦工作完成,完成处理程序将排队在事件循环中运行。Boost.Asio 虽然没有提供线程池,但 io_service 可以通过允许多个线程调用 run 来轻松充当线程池。这将线程管理和行为的责任转移给用户,如本例所示。
线程和同步
libuv 提供线程和同步类型的抽象。Boost.Thread 提供线程和同步类型。这些类型中的许多都紧跟 C++11 标准,但也提供了一些扩展。由于 Boost.Asio 允许多个线程运行单个事件循环,它提供了 strands 作为创建事件处理程序顺序调用的一种手段,而无需使用显式锁定机制。
文件系统操作
libuv 提供许多文件系统操作的抽象。每个操作都有一个函数,每个操作都可以是同步阻塞或异步的。如果提供了回调,则操作将在内部线程池中异步执行。如果没有提供回调,则调用将是同步阻塞的。Boost.Filesystem 为许多文件系统操作提供同步阻塞调用。这些可以与 Boost.Asio 和线程池结合起来创建异步文件系统操作。
网络
libuv 支持 UDP 和 TCP 套接字的异步操作以及 DNS 解析。应用程序开发人员应注意,底层文件描述符设置为非阻塞。因此,本机同步操作应检查返回值和 errno 是否为 EAGAIN 或 EWOULDBLOCK。Boost.Asio 在网络支持方面更为丰富。除了 libuv 的网络功能外,Boost.Asio 还支持 SSL 和 ICMP 套接字。此外,Boost.Asio 提供同步阻塞和同步非阻塞操作,除了异步操作外,还有许多独立函数提供常见的高级操作,例如读取指定数量的字节,或直到读取到指定的分隔符字符。
信号
libuv 通过 uv_signal_t 类型和 uv_signal_* 操作提供信号处理和信号发送的抽象。Boost.Asio 不提供信号发送的抽象,但其 signal_set 提供信号处理。
进程间通信
libuv 通过单个 uv_pipe_t 类型抽象 Unix 域套接字和 Windows 命名管道。Boost.Asio 将两者分开为 local::stream_protocol::socket 或 local::datagram_protocol::socket,以及 windows::stream_handle。
API 差异
虽然 API 因语言而异,但以下是一些关键差异:
操作和处理程序关联
在 Boost.Asio 中,操作和处理程序之间是一对一的映射。例如,每个 async_write 操作将调用 WriteHandler 一次。这对于 libuv 的许多操作和处理程序来说也是如此。然而,libuv 的 uv_async_send 支持多对一映射。多个 uv_async_send 调用可能会导致 uv_async_cb 被调用一次。
调用链与观察者循环
在处理任务时,如从流/UDP 读取、处理信号或等待定时器,Boost.Asio 的异步调用链更为明确。在 libuv 中,创建一个观察者来指定对特定事件的兴趣。然后为观察者启动一个循环,其中提供一个回调。在接收到感兴趣的事件时,将调用回调。另一方面,Boost.Asio 需要每次应用程序对处理事件感兴趣时发出操作。
为了帮助说明这一差异,以下是使用 Boost.Asio 的异步读取循环,其中多次发出 async_receive 调用:
void start()
{
socket.async_receive(buffer, handle_read); ----.
} |
.--------------------------------------------'
| .-------------------------------------.
V V |
void handle_read(...) |
{ |
std::cout << "got data" << std::endl; |
socket.async_receive(buffer, handle_read); --'
}
以下是相同的 libuv 示例,其中每次观察者观察到套接字有数据时调用 handle_read:
uv_read_start(socket, alloc_buffer, handle_read); --.
|
.-----------------------------------------------'
|
V
void handle_read(...)
{
fprintf(stdout, "got data\n");
}
内存分配
由于 Boost.Asio 中的异步调用链和 libuv 中的观察者,内存分配通常发生在不同的时间。使用观察者时,libuv 将分配推迟到接收到需要处理的事件后。分配通过用户回调完成,在 libuv 内部调用,并将释放责任推迟给应用程序。另一方面,Boost.Asio 的许多操作要求在发出异步操作之前分配内存,例如 async_read 的缓冲区。Boost.Asio 提供 null_buffers,可用于监听事件,允许应用程序将内存分配推迟到需要时,尽管这已被弃用。
这种内存分配差异也存在于 bind->listen->accept 循环中。使用 libuv,uv
_listen 创建一个事件循环,在连接准备好接受时调用用户回调。这允许应用程序将客户端分配推迟到尝试连接时。另一方面,Boost.Asio 的 listen 仅更改接收器的状态。async_accept 监听连接事件,并要求在调用之前分配对等方。
性能
不幸的是,我没有具体的基准数据来比较 libuv 和 Boost.Asio。然而,我在实时和近实时应用程序中使用这些库时观察到类似的性能。如果需要硬性数据,libuv 的基准测试可能是一个起点。
此外,虽然应进行性能分析以识别实际瓶颈,但要注意内存分配。对于 libuv,内存分配策略主要限于分配器回调。另一方面,Boost.Asio 的 API 不允许分配器回调,而是将分配策略推给应用程序。然而,Boost.Asio 中的处理程序/回调可能会被复制、分配和释放。Boost.Asio 允许应用程序提供自定义内存分配函数,以便为处理程序实现内存分配策略。
成熟度
Boost.Asio
Asio 的开发至少可以追溯到 2004 年 10 月,并于 2006 年 3 月 22 日在经过为期 20 天的同行评审后被接受为 Boost 1.35 的一部分。它还作为 TR2 网络库提案的参考实现和 API。Boost.Asio 有相当多的文档,但其有用性因用户而异。
API 也有相当一致的感觉。此外,异步操作在操作名称中明确。例如,accept 是同步阻塞的,而 async_accept 是异步的。API 提供了用于常见 I/O 任务的自由函数,例如读取流中的数据直到读取到 \r\n。还注意隐藏一些特定于网络的细节,例如 ip::address_v4::any() 表示 0.0.0.0 的 "所有接口" 地址。
最后,Boost 1.47+ 提供处理程序跟踪,在调试时非常有用,以及 C++11 支持。
libuv
根据它们的 GitHub 图表,Node.js 的开发至少可以追溯到 2009 年 2 月,而 libuv 的开发则追溯到 2011 年 3 月。《uvbook》是 libuv 入门的好地方。API 文档在这里。
总体而言,API 相当一致且易于使用。一个可能引起混淆的异常是 uv_tcp_listen 创建一个观察者循环。这不同于其他通常具有 uv_start 和 uvstop 成对函数来控制观察者循环生命周期的观察者。此外,一些 uv_fs* 操作有相当多的参数(最多 7 个)。由于同步和异步行为取决于是否存在回调(最后一个参数),同步行为的可见性可能会降低。
最后,快速浏览 libuv 的提交历史记录显示,开发人员非常活跃。