2.libuv基础

libuv基础:

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

事件循环:

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

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

以下是一些事件的例子:

  • 文件已准备好写入
  • 套接字有可以读取的数据
  • 计时器超时

这个事件循环被uv_run()封装——使用libuv时的end-all函数。

系统程序最常见的活动是处理输入和输出,而不是大量的数字运算。使用传统输入/输出函数(read, fprintf等)的问题是它们是阻塞的。与处理器的速度相比,实际写入硬盘或从网络读取所需的时间长得不成比例。函数直到任务完成后才返回,因此您的程序什么也不做。对于要求高性能的程序,这是一个主要的障碍,因为其他活动和其他I/O操作一直在等待。

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

libuv所遵循的方法使用了另一种风格,即异步、非阻塞风格。大多数现代操作系统都提供事件通知子系统。例如,对套接字的一个普通的read调用会阻塞,直到发送方实际发送了一些东西。相反,应用程序可以请求操作系统监视套接字并在队列中放入事件通知。应用程序可以方便地检查事件(可能在最大限度使用处理器之前做一些数字运算)并获取数据。它是异步的,因为应用程序在某一点表示兴趣,然后在另一点(在时间和空间上)使用数据。它是非阻塞的,因为应用程序进程可以自由地执行其他任务。这很符合libuv的事件循环方法,因为操作系统事件可以被视为另一个libuv事件。非阻塞确保其他事件可以像在[1]中一样快速地被处理。

  • 注释:I / O是如何在后台运行并不是我们关心的,但由于我们的计算机硬件的工作方式,与线程处理器的基本单位,libuv和操作系统通常会运行背景/工作线程和/或轮询以非阻塞的方式执行任务。

Bert Belder, libuv核心开发者之一,有一个小视频解释libuv的架构和它的背景。如果你之前没有libuv或libev的经验,这是一个快速,有用的视频。

Hello World:

 了解了这些基本知识之后,让我们编写第一个libuv程序。它什么也不做,除了启动一个将立即退出的循环。

helloworld/main.c

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <uv.h>
 4 
 5 int main() {
 6     uv_loop_t *loop = malloc(sizeof(uv_loop_t));
 7     uv_loop_init(loop);
 8 
 9     printf("Now quitting.\n");
10     uv_run(loop, UV_RUN_DEFAULT);
11 
12     uv_loop_close(loop);
13     free(loop);
14     return 0;
15 }

此程序立即退出,因为它没有事件要处理。必须告诉libuv事件循环注意使用各种API函数的事件。

从libuv v1.0开始,用户应该在使用uv_loop_init(uv_loop_t *)初始化之前为循环分配内存。这允许您插入自定义内存管理。记住使用uv_loop_close(uv_loop_t *)来反初始化循环,然后删除存储。这些例子从来没有关闭循环,因为在循环结束后程序将退出,系统将回收内存。

生产级项目,特别是长时间运行的系统程序,应该注意正确发布。

默认循环:

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

  • 注意:Node.js使用默认循环作为其主循环。如果您正在编写绑定,则应该注意这一点。

错误处理:

可能失败的初始化函数或同步函数在出错时返回一个负数。可能失败的异步函数将向它们的回调函数传递一个状态参数。错误消息被定义为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 监视器:

 1 /* Handle types. */
 2 typedef struct uv_loop_s uv_loop_t;
 3 typedef struct uv_handle_s uv_handle_t;
 4 typedef struct uv_dir_s uv_dir_t;
 5 typedef struct uv_stream_s uv_stream_t;
 6 typedef struct uv_tcp_s uv_tcp_t;
 7 typedef struct uv_udp_s uv_udp_t;
 8 typedef struct uv_pipe_s uv_pipe_t;
 9 typedef struct uv_tty_s uv_tty_t;
10 typedef struct uv_poll_s uv_poll_t;
11 typedef struct uv_timer_s uv_timer_t;
12 typedef struct uv_prepare_s uv_prepare_t;
13 typedef struct uv_check_s uv_check_t;
14 typedef struct uv_idle_s uv_idle_t;
15 typedef struct uv_async_s uv_async_t;
16 typedef struct uv_process_s uv_process_t;
17 typedef struct uv_fs_event_s uv_fs_event_t;
18 typedef struct uv_fs_poll_s uv_fs_poll_t;
19 typedef struct uv_signal_s uv_signal_t;
20 
21 /* Request types. */
22 typedef struct uv_req_s uv_req_t;
23 typedef struct uv_getaddrinfo_s uv_getaddrinfo_t;
24 typedef struct uv_getnameinfo_s uv_getnameinfo_t;
25 typedef struct uv_shutdown_s uv_shutdown_t;
26 typedef struct uv_write_s uv_write_t;
27 typedef struct uv_connect_s uv_connect_t;
28 typedef struct uv_udp_send_s uv_udp_send_t;
29 typedef struct uv_fs_s uv_fs_t;
30 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()现在是如何阻塞的,因为存在一个监控器。空闲监视程序在达到计数时停止,uv_run()退出,因为没有活动的事件监视程序。

 idle-basic/main.c

 1 #include <stdio.h>
 2 #include <uv.h>
 3 
 4 int64_t counter = 0;
 5 
 6 void wait_for_a_while(uv_idle_t* handle) {
 7     counter++;
 8 
 9     if (counter >= 10e6)
10         uv_idle_stop(handle);
11 }
12 
13 int main() {
14     uv_idle_t idler;
15 
16     uv_idle_init(uv_default_loop(), &idler);
17     uv_idle_start(&idler, wait_for_a_while);
18 
19     printf("Idling...\n");
20     uv_run(uv_default_loop(), UV_RUN_DEFAULT);
21 
22     uv_loop_close(uv_default_loop());
23     return 0;
24 }

存储环境

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

 
posted @ 2021-05-06 15:59  风吹大风车  阅读(545)  评论(0)    收藏  举报