代码改变世界

libuv 中文编程指南(二)libuv 基础

2013-03-17 21:57  Haippy  阅读(50064)  评论(0编辑  收藏  举报

Libuv 基础

libuv 采用了 异步 (asynchronous), 事件驱动 (event-driven)的编程风格, 其主要任务是为开人员提供了一套事件循环和基于I/O(或其他活动)通知的回调函数, libuv 提供了一套核心的工具集, 例如定时器, 非阻塞网络编程的支持, 异步访问文件系统, 子进程以及其他功能.

事件循环(Event loops)

在事件编程模型中, 应用程序通常会关注某些特定的事件, 并在事件发生后对其作出响应. 而收集事件或监控其他事件源则是 libuv 的职责, 编程人员只需要对感兴趣的事件注册回调函数, 在事件发生后 libuv 将会调用相应的回调函数. 只要程序不退出(被系统管理人员 kill 掉), 事件循环通常会一直运行, 下面是事件驱动编程模型的伪代码:

while there are still events to process:
    e = get the next event
    if there is a callback associated with e:
        call the callback

适用于事件驱动编程模型的例子如下:

  • 文件已经准备好可写入数据.
  • 某一 socket 上存在数据可读.
  • 定时器已超时.

事件循环由 uv_run 函数封装, 在使用 libuv 编程时, 该函数通常在最后才被调用.

计算机程序最基本的活动是输入输出的处理, 而不是大量的数值计算, 而使用传统输入输出函数(read, fprintf 等)的问题是它们都是 阻塞 的. 将数据写入磁盘或者从网络读取数据都会消耗大量时间, 而阻塞函数直到任务完成后才返回, 在此期间你的程序什么也没有做, 浪费了大量的 CPU 时间. 对于追求高性能的程序而言, 在其他活动或者 I/O 操作在进行尽量让 CPU 不被阻塞.

标准的解决方案是使用线程, 每个阻塞的 I/O 操作都在一个单独的线程(或线程池)中启动, 当阻塞函数被调用时, 处理器可以调度另外一个真正需要 CPU 的线程来执行任务.

Libuv 采用另外一种方式处理阻塞任务, 即 异步非阻塞 方式.大多数现代操作系统都提供了事件通知功能, 例如, 调用 read 读取网络套接字时程序会阻塞, 直到发送者最终发送了数据(read 才返回). 但是, 应用程序可以要求操作系统监控套接字, 并在套接字上注册事件通知. 应用程序可以在适当的时候查看它所监视的事件并获取数据(若有). 整个过程是 异步 的, 因为程序在某一时刻关注了它感兴趣的事件, 并在另一个时刻获取(使用)数据, 这也是 非阻塞 的, 因为该进程还可以处理另外的任务. Libuv 的事件循环方式很好地与该模型匹配, 因为操作系统事件可以视为另外一种 libuv 事件. 非阻塞方式可以保证在其他事件到来时被尽快处理 [1].

Note

I/O 是如何在后台运行的不是我们所关心的, 但是由于我们计算机硬件的工作方式, 线程是处理器最基本的执行单元, thread as the basic unit of the , libuv 和操作系统通常会运行后台/工作者线程, 或者采用非阻塞方式来轮流执行任务.

Hello World

具备了上面最基本的知识后, 我们就来编写一个简单 libuv 的程序吧.该程序并没有做任何具体的事情, 只是简单的启动了一个会退出的事件循环.

#include <stdio.h>
#include <uv.h>

int main() {
    uv_loop_t *loop = uv_loop_new();

    printf("Now quitting.\n");
    uv_run(loop, UV_RUN_DEFAULT);

    return 0;
}

该程序启动后就会直接退出, 因为你没有事件可处理. 我们可以使用 libuv 提供了各种 API 来告知 libuv 我们感兴趣的事件.

libuv 的默认事件循环(Default loop)

libuv 提供了一个默认的事件循环, 你可以通过 uv_default_loop 来获得该事件循环, 如果你的程序中只有一个事件循环, 你就应该使用 libuv 为我们提供的默认事件循环.

Note

node.js 使用默认事件循环作为它的主循环,如果你正在编写 node.js 的绑定, 你应该意识到这一点.

监视器(Watchers)

libuv 通过监视器(Watcher)来对特定事件进行监控, 监视器通常是类似 uv_TYPE_t 结构体的封装, TYPE 代表该监视器的用途, libuv 所有的监视器类型如下:

typedef struct uv_loop_s uv_loop_t;
typedef struct uv_err_s uv_err_t;
typedef struct uv_handle_s uv_handle_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;

所有监视器的结构都是 uv_handle_t 的”子类”, 在 libuv 和本文中都称之为句柄( handlers ).

监视器由相应类型的初始化函数设置, 如下:

uv_TYPE_init(uv_TYPE_t*) 

某些监视器初始化函数的第一个参数为事件循环的句柄.

监视器再通过调用如下类型的函数来设置事件回调函数并监听相应事件:

uv_TYPE_start(uv_TYPE_t*, callback) 

而停止监听应调用如下类型的函数:

uv_TYPE_stop(uv_TYPE_t*) 

当 libuv 所监听事件发生后, 回调函数就会被调用. 应用程序特定的逻辑通常都是在回调函数中实现的, 例如, 定时器回调函数在发生超时事件后也会被调用, 另外回调函被调用时传入的相关参数都与特定类型的事件有关, 例如, IO 监视器的回调函数在发生了IO事件后将会收到从文件读取的数据.

空转(Idling)

接下来我们通过例子来讲述监视器的使用. 例子中空转监视器回调函数被不断地重复调用, 当然其中也有一些深层次的语言,我们将会在 工具集 进一步讨论, 但现在我们只是跳过具体细节. 我们只是使用了一个空转监视器回调来看看监视器的生命周期, 通过例子我们也可以了解到: 由于设置了监视器, 所以调用 uv_run() 是程序会阻塞, 空转监视器将会在计数器达到设定的值时停止(监视), uv_run() 会退出因为此时程序中没有活动的监视器了.

#include <stdio.h>
#include <uv.h>

int64_t counter = 0;

void wait_for_a_while(uv_idle_t* handle, int status) {
    counter++;

    if (counter >= 10e6)
        uv_idle_stop(handle);
}

int main() {
    uv_idle_t idler;
    uv_idle_init(uv_default_loop(), &idler);
    uv_idle_start(&idler, wait_for_a_while);

    printf("Idling...\n");
    uv_run(uv_default_loop(), UV_RUN_DEFAULT);

    return 0;
}