2.2.5 libuv三个队列

861 阅读6分钟

介绍

image.png

Libevent、libev、libuv三个网络库,都是c语言实现的异步事件库

libevent :名气最大,应用最广泛,历史悠久的跨平台事件库;

libev :较libevent而言,设计更简练,性能更好,但对Windows支持不够好;

libuv :开发node的过程中需要一个跨平台的事件库,他们首选了libev,但又要支持Windows,故重新封装了一套,linux下用libev实现,Windows下用IOCP实现;

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

nodejs启动过程

之前提到node启动后会依次 加载原生模块、初始化js运行环境、加载自定义模块后、执行用户代码、进入libuv事件循环

三个队列

libuv的实现是一个很经典生产者-消费者模型。
只要程序不退出(被系统管理人员 kill 掉), 事件循环通常会一直运行。

  • 观察者队列:观察者主要是保存了io相关的文件描述符、回调、感兴趣的事件等信息。所有需要libuv处理的io观察者都挂载在这个队列里,等待事件循环取出。 image.png
  • 任务队列:一次异步 I/O 会把请求对象放在任务队列中,首先会判断当前线程池是否有可用的线程,如果线程可用,那么会执行任务队列请求对象的 I/O 操作。已经完成的 I/O 对象提交到I/O 观察者列队。
  • 事件队列:libuv将观察者的回调任务放入事件队列wq,唤醒js进行执行。

image.png

  • event queue调用v8执行js.
  • 网络请求uv_listen插入到watcher queue.
  • fs异步调用,封装请求对象放入task queue.
  • task执行完毕,提交到watcher queue.
  • 通知主线程polling watcher queue,回调提交到event queue,回到调用v8执行js.

观察者队列

例如http网络中,uv_io_start()负载将 uv_listen 插入到处理的watcher queue中, uv_listen 注册一个事件回调,当新的对端连接到这个套接字时,事件循环Polling观察者队列,将回调函数插入到事件队列里面,唤醒js执行。

var http = require('http'); 
var fs = require('fs'); 
var url = require('url'); 
// 创建服务器 
http.createServer( function (request, response) { 
    // 解析请求,包括文件名 
    var pathname = url.parse(request.url).pathname; 
    // 输出请求的文件名 
    console.log("Request for " + pathname + " received."); 
    // 从文件系统中读取请求的文件内容 
    fs.readFile(pathname.substr(1), function (err, data) { 
    if (err) { 
        // HTTP 状态码: 404 : NOT FOUND 
        response.writeHead(404, {'Content-Type': 'text/html'}); 
    }else{ 
        // HTTP 状态码: 200 : OK 
        response.writeHead(200, {'Content-Type': 'text/html'}); 
        // 响应文件内容 
        response.write(data.toString()); } 
        // 发送响应数据 
        response.end(); 
    });
}).listen(8080);

image.png

libuv的一个小例子

#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;
     // 获取事件循环的核心结构体。并初始化一个idler
    uv_idle_init(uv_default_loop(), &idler);
    // 往事件循环的idle节点插入一个任务
    uv_idle_start(&idler, wait_for_a_while);
    // 启动事件循环
    uv_run(uv_default_loop(), UV_RUN_DEFAULT);
    // 销毁libuv的相关数据
    uv_loop_close(uv_default_loop());
    return 0;
}

任务队列与线程池

Libuv的线程包括2部分,一个是主线程,一个是线程池。 主线程的一部分工作是描述任务并将其提交给线程池,线程池进行处理。

  • 任务队列:一次异步 I/O 会把请求对象放在任务队列中,首先会判断当前线程池是否有可用的线程,如果线程可用,那么会执行任务队列请求对象的 I/O 操作。
    拿异步文件操作为例,
    1. 主线程作为生产者,生成一个描述文件操作的对象,将其提交到任务队列
    1. 线程池从任务队列获取该对象进行处理。线程池中的线程是消费者,消费任务队列里的任务;任务队列是生产者和消费者之间的桥梁;
    1. 线程池执行完任务后,将结果交给主线程,主线程拿到结果后,如果发现有回调函数需要执行,就执行。

image.png

提交任务

主线程将任务提交到任务队列是通过uv__work_submit来实现的,让我们来看下它的代码:

struct uv__work {
  void (*work)(struct uv__work *w);
  void (*done)(struct uv__work *w, int status);
  struct uv_loop_s* loop;
  void* wq[2]; // 用于将其关联到任务队列中
};

void uv__work_submit(uv_loop_t* loop,
                     struct uv__work* w,
                     enum uv__work_kind kind,
                     void (*work)(struct uv__work* w),
                     void (*done)(struct uv__work* w, int status)) {
  uv_once(&once, init_once); // 【初始化线程】,无乱调用多少次,init_once只会执行一次
  w->loop = loop; // 事件循环
  w->work = work; // 线程池要执行的函数
  w->done = done; // 线程池执行结束后,通知主线程要执行的函数
  post(&w->wq, kind); // 将任务提交任务队列中
}

消费任务

任务队列中的任务是通过线程池进行消费的,而线程池的初始化是在uv__work_submit调用init_once实现的; 初始化线程池会先获取线程池的大小nthreads;然后初始化互斥量mutex、条件变量cond和任务队列wq;最后创建nthreads个线程,每个线程执行worker()函数消费任务队列里的任务;
worker()函数的本质就是从任务队列中获取任务,然后执行work函数。执行完后,将该任务提交到事件循环loop的wp队列中,通过uv_async_send告知主线程执行任务中的done函数。

static void worker(void* arg) {
  struct uv__work* w;
  QUEUE* q;
  
  ...
  arg = NULL;
    
  // 获取互斥锁
  uv_mutex_lock(&mutex);
    
  // 通过无限循环,保证线程一直执行
  for (;;) {
    
    // 如果任务队列为空,通过等待条件变量cond挂起,并释放锁mutex
    // 主线程提交任务通过uv_cond_signal唤起,并重新获取锁mutex
    while (QUEUE_EMPTY(&wq) || ...) {
      idle_threads += 1;
      uv_cond_wait(&cond, &mutex);
      idle_threads -= 1;
    }
      
    // 从任务队列中获取第一个任务
    q = QUEUE_HEAD(&wq);
    ...
        
    // 将该任务从任务队列中删除
    QUEUE_REMOVE(q);
    QUEUE_INIT(q);
      
    ...
        
    // 操作完任务队列,释放锁mutex
    uv_mutex_unlock(&mutex);

    // 获取uv__work对象,并执行work
    w = QUEUE_DATA(q, struct uv__work, wq);
    w->work(w);

    // 获取loop的互斥锁wq_mutex
    uv_mutex_lock(&w->loop->wq_mutex);
    w->work = NULL;
    
    // 将执行完work函数的任务挂到loop->wq队列中
    QUEUE_INSERT_TAIL(&w->loop->wq, &w->wq);
      
    // 通过uv_async_send通知主线程,当然有任务执行完了,主线程可以执行任务中的done函数。
    uv_async_send(&w->loop->wq_async);
    uv_mutex_unlock(&w->loop->wq_mutex);

    // 获取锁,执行任务队列中的下一个任务
    ...
    uv_mutex_lock(&mutex);
    ...
  }
}

执行回调

事件循环loop在初始化的时候会调用uv_async_init,其他线程调用uv_async_send会执行uv__work_done。
uv__work_done主要逻辑是 获取队列中的每个任务并唤醒js函数执行。

uv_async_init(loop, &loop->wq_async, uv__work_done);

参考: zhuanlan.zhihu.com/p/91183396

欢迎关注我的前端自检清单,我和你一起成长