libuv之二:libuv基础

Posted 风吹大风车

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了libuv之二:libuv基础相关的知识,希望对你有一定的参考价值。

libuv基础

libuv强制采用异步的、事件驱动的编程风格。它的核心是提供一个事件循环和I/O通知的回调和其他活动。libuv提供了核心实用工具如定时器,非阻塞网络支持,异步文件系统访问,子进程等。

事件循环

事件驱动编程中,应用程序对某些事件感兴趣,并在事件发生时对其作出响应,libuv负责从操作系统收集事件或监视其他事件来源,用户可以注册回调,以便在事件发生时调用。事件循环通常会永远运行下去。伪代码如下:

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

一些事件的例子:

. 文件准备好待写时
. 套接字有数据待读时
. 定时器超时时

这个事件循环被libuv的uv_run()封装,end-all函数。
系统程序最常见的活动是处理输入和输出,而不是大量的数字运算。使用传统输入/输出函数(read, fprintf等)的问题是它们是阻塞的。与处理器的速度相比,实际写入硬盘或从网络读取所需的时间长得不成比例。函数直到任务完成后才返回,在此期间您的程序什么也不做。对于要求高性能的程序,这是一个主要的障碍,因为其他活动和其他I/O操作一直在等待。
标准的解决方案之一是使用线程。每个阻塞I/O操作都在单独的线程(或线程池)中启动。当阻塞函数在线程中被调用时,处理器可以调度另一个真正上需要CPU的线程运行。
libuv所遵循的方法使用了另一种风格,即异步、非阻塞风格。大多数现代操作系统都提供事件通知子系统。例如,对套接字的一个普通的read调用会阻塞,直到发送方实际发送了一些东西。相反,应用程序可以请求操作系统监视套接字并在队列中放入事件通知。应用程序可以在它方便的时候检查事件(也许在最大限度使用处理器之前做一些数字运算)并获取数据。它是异步的,因为它在某一点感兴趣,然后在另一点(在时间或空间上)使用数据。它是非阻塞的,因为应用程序进程可以自由地执行其他任务。这很符合libuv的事件循环方法,因为操作系统事件可以被视为另一个libuv事件。非阻塞确保其他事件可以在他们刚进来的时候就被处理到。

注意:I/O是如何在后台运行并不是我们关心的,但根据我们的计算机硬件的工作方式,与线程处理器的基本单位,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;
}

此程序立即退出,因为它没有事件要处理。一个libuv事件循环是必须要先使用各式API接口去设置监听那些事件。
从libuv v1.0开始,用户应该在使用初始化uv_loop_init(uv_loop_t )之前为循环申请分配内存。这让你去自定义内存管理。记得用 uv_loop_close(uv_loop_t ) 去释放循环,然后释放内存。这些例子从来没有关闭循环,因为在循环结束后程序将退出,系统将回收内存。生产项目级别的项目尤其是长时间运行的系统程序,需要特别注意正确的释放。

默认循环

libuv提供了一个默认循环,可以使用uv_default_loop()访问,如果你只想要一个循环,你可以使用这个默认循环。

注意:nodejs使用默认循环作为他的主要循环,如果你正在写绑定关系,你应该注意这一点。

错误处理

初始化函数或同步函数有时候会返回一个负数的错误码。异步函数如果失败会传递一个状态参数给它们的回调函数。错误信息都定义在UV_E*的常量中。
你可以使用uv_strerror(int)和uv_err_name(int)函数分别得到一个const char *类型的错误或错误名称。
I/O回调(比如文件或套接字)会受到一个nread参数,如果nread参数小于0,这就表示一个错误(UV_EOF文件末尾错误,你可能希望分别处理)

处理和请求

libuv由对特定事件感兴趣的用户工作,这通常是通过创建一个I/O设备、定时器或进程的句柄来完成的。句柄是一个 名为uv_TYPE_t类型的封闭结构,其中type表示句柄的用途。
libuv监视器

/* 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观察器的回调将接收从文件读取的数据,计时器回调将在超时时触发等。

空转

下面是一个使用空闲句柄的例子。callback在事件循环的每一个回合中被调用一次。在实用程序中讨论了空闲句柄的用例。让我们使用一个空闲的监控器来查看监控器的生命周期,并看看uv_run()是如何阻塞的,因为监视器就是现在。空闲的监视器在count收到的时候停止,uv_run()在没有事件监视器激活后会退出。
idle-basic/main.c

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

int64_t counter = 0;

void wait_for_a_while(uv_idle_t* handle) {
    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);

    uv_loop_close(uv_default_loop());
    return 0;
}

存储上下文

在基于回调的编程风格中你同时希望在调用站点和回调之间传递一些应用程序特性内容的上下文信息。所有的句柄和请求都有一个void*数据成员,你可以将其设置为上下文,并在回调中进行强制转换。这是整个C库生态系统中使用的一种常见模式。此外,uv_loop_t也有一个类似的数据成员。

以上是关于libuv之二:libuv基础的主要内容,如果未能解决你的问题,请参考以下文章

从libuv源码学习线程池

编译Libuv

什么是libuv?

libuv vs asyncio (python)

libuv之一:libuv介绍

libuv:fork 和 uv_spawn 的区别?