文章系列(部分):
Basics of libuv
libuv 执行异步的、事件驱动的编程风格。 它的核心工作是提供一个事件循环和基于 I/O 及其他活动的通知的回调。 libuv 提供了诸如定时器、非阻塞网络支持、异步文件系统访问、子进程等核心组件。
Event loops
在事件驱动编程中,应用程序表达对某些事件的兴趣并在它们发生时做出响应。 从操作系统收集事件或监视其他事件源的责任由 libuv 处理,用户可以注册回调以在事件发生时调用。 事件循环通常会一直运行下去。 表示在伪代码中:
while there are still events to process:
e = get the next event
if there is a callback associated with e:
call the callback
Some examples of events are:
- File is ready for writing
- A socket has data ready to be read
- A timer has timed out
此事件循环由 uv_run()
封装——使用 libuv 时的最终功能。
系统程序最常见的活动是处理输入和输出,而不是大量的数字运算。使用传统输入/输出函数(read、fprintf 等)的问题在于它们是阻塞的。与处理器的速度相比,实际写入硬盘或从网络读取所需的时间非常长。在任务完成之前,函数不会返回,因此您的程序什么也不做。对于需要高性能的程序,这是一个主要障碍,因为需要等待 I/O 操作。
标准解决方案之一是使用线程。每个阻塞 I/O 操作都在单独的线程(或线程池)中启动。当线程中调用阻塞函数时,操作系统可以调度另一个线程运行,这实际上需要CPU。
libuv 所遵循的方法使用了另一种风格,即异步、非阻塞风格。大多数现代操作系统都提供事件通知子系统。例如,套接字上的正常读取调用会阻塞,直到发送者实际发送了一些东西。相反,应用程序可以请求操作系统监视套接字并将事件通知放入队列中。应用程序可以在方便时检查事件(在此之前可以执行一些计算任务)并获取数据。它是异步的,因为应用程序在某一点表达了兴趣,然后在另一点(时间和空间)使用了数据。它是非阻塞的,因为应用程序进程可以自由地执行其他任务。这与 libuv 的事件循环方法非常吻合, the operating system events can be treated as just another libuv event.。非阻塞确保了其他事件可以继续以尽可能快的速度处理。
注意:I/O如何在后台运行不是我们关心的,但是由于我们计算机硬件的工作方式, 即以线程为处理器的基本单元,libuv和操作系统通常会运行background/worker threads and/or 轮询以非阻塞方式执行任务。
Bert Belder,libuv 核心开发人员之一,有一个小视频解释了 libuv 的架构及其背景。 如果您之前没有使用 libuv 或 libev 的经验,那么它是一个快速、有用的教程。
libuv 的事件循环在此文档中有更详细的解释。
Hello World
有了基础知识,让我们编写我们的第一个 libuv 程序。 它什么也不做,除了启动一个将立即退出的循环。
helloworld/main.c
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
int main() {
uv_loop_t *loop = malloc(sizeof(uv_loop_t));
uv_loop_init(loop);
printf("Now quitting.\n");
uv_run(loop, UV_RUN_DEFAULT);
uv_loop_close(loop);
free(loop);
return 0;
}
该程序立即退出,因为它没有要处理的事件。 必须使用各种 API 函数告诉 libuv 事件循环需要监控的事件。
从 libuv v1.0 开始,用户应该在使用 uv_loop_init(uv_loop_t *)
初始化之前为循环分配内存。这允许您插入自定义内存管理。 请记住使用 uv_loop_close(uv_loop_t *)
取消初始化循环,然后删除存储。本文教程的实例没有关闭循环, 因为程序在循环结束后退出并且系统将回收内存。生产级项目,尤其是长期运行的系统程序,应注意正确释放内存, 避免内存泄漏。
Default loop
libuv 提供了一个默认循环,可以使用 uv_default_loop() 访问。 如果你只想要一个循环,你应该使用这个循环。
默认循环, 应该是一个已经申请了内存空间并执行了初始化的, 可以直接通过 uv_default_loop()
返回一个 uv_loop_t *
的变量. 但是需要我们手动的释放.
注意:node.js 使用默认循环作为其主循环。 如果您正在编写bindings,则应该注意这一点。
Error handling
初始化函数, 或者异步函数可能会执行失败, 失败后将返回一个负数. 对于异步函数, 会将失败的状态参数返回给回调函数. 错误信息被定义为一个常量+错误描述信息, 详细见:Error handling
以使用 uv_strerror(int)
和 uv_err_name(int)
函数分别获取错误描述或错误名称的 const char *
。
I/O 读取回调(例如文件和套接字)被传递一个参数 nread。 如果 nread 小于 0,则存在错误(UV_EOF 是文件结束错误,您可能希望以不同方式处理)。
Handles and Requests
libuv会监听用户感兴趣的事件, 在实现上, 这通常是通过创建I/O 设备、计时器或进程的句柄来完成。 句柄是命名为 uv_TYPE_t 的不透明结构,其中TYPE表示句柄的用途。
/* Handle types. */
typedef struct uv_loop_s uv_loop_t;
typedef struct uv_handle_s uv_handle_t;
typedef struct uv_dir_s uv_dir_t;
typedef struct uv_stream_s uv_stream_t;
typedef struct uv_tcp_s uv_tcp_t;
typedef struct uv_udp_s uv_udp_t;
typedef struct uv_pipe_s uv_pipe_t;
typedef struct uv_tty_s uv_tty_t;
typedef struct uv_poll_s uv_poll_t;
typedef struct uv_timer_s uv_timer_t;
typedef struct uv_prepare_s uv_prepare_t;
typedef struct uv_check_s uv_check_t;
typedef struct uv_idle_s uv_idle_t;
typedef struct uv_async_s uv_async_t;
typedef struct uv_process_s uv_process_t;
typedef struct uv_fs_event_s uv_fs_event_t;
typedef struct uv_fs_poll_s uv_fs_poll_t;
typedef struct uv_signal_s uv_signal_t;
/* Request types. */
typedef struct uv_req_s uv_req_t;
typedef struct uv_getaddrinfo_s uv_getaddrinfo_t;
typedef struct uv_getnameinfo_s uv_getnameinfo_t;
typedef struct uv_shutdown_s uv_shutdown_t;
typedef struct uv_write_s uv_write_t;
typedef struct uv_connect_s uv_connect_t;
typedef struct uv_udp_send_s uv_udp_send_t;
typedef struct uv_fs_s uv_fs_t;
typedef struct uv_work_s uv_work_t;
句柄代表长寿命的对象。 此类句柄上的异步操作是使用请求标识的。 一个请求是短暂的(通常只在一个回调中使用)并且通常表示一个句柄上的一个 I/O 操作。 请求用于保留各个操作的启动和回调之间的上下文。 例如,UDP 套接字由 uv_udp_t 表示,而对套接字的单独写入使用 uv_udp_send_t 结构,该结构在写入完成后传递给回调。
句柄由相应的设置:
uv_TYPE_init(uv_loop_t *, uv_TYPE_t *)
回调是当观察者感兴趣的事件发生时由 libuv 调用的函数。 应用程序特定的逻辑通常会在回调中实现。 例如,IO watcher 的回调将接收从文件读取的数据,定时器回调将在超时时触发等等。
Idling
这是使用Idle句柄的示例。 回调在事件循环的每一轮中调用一次。
#include <stdio.h>
#include <uv.h>
// 一个计数器
int64_t counter = 0;
// Idle 句柄的回调函数, 参数是句柄本身
// 回调函数, 将在计数器超过一定数值后关闭Idle句柄
void wait_for_a_while(uv_idle_t* handle) {
counter++;
if (counter >= 10e6)
uv_idle_stop(handle);
}
int main() {
// 定义Idle句柄
uv_idle_t idler;
// 用默认循环, 初始化Idle句柄, 此时默认循环将观察到该句柄, 但是没有激活
uv_idle_init(uv_default_loop(), &idler);
// 激活Idle句柄, 并指定回调函数
uv_idle_start(&idler, wait_for_a_while);
printf("Idling...\n");
// 开始执行循环, UV_RUN_DEFAULT表示只要有激活的事件,就反复循环
uv_run(uv_default_loop(), UV_RUN_DEFAULT);
// Idle句柄被关闭, 循环发现没有激活的句柄了, 就退出.
// 手动关闭默认循环.
uv_loop_close(uv_default_loop());
return 0;
}
Storing context
In callback based programming style you’ll often want to pass some context
– application specific information – between the call site and the callback. All handles and requests have a void* data
member which you can set to the context and cast back in the callback. This is a common pattern used throughout the C library ecosystem. In addition uv_loop_t
also has a similar data member.
Pingback:libuv : User guide » Utilities – Kingdo Station