浅析libuv源码-node事件轮询解析(1)

  好久没写东西了,过了一段咸鱼生活,无意中想起了脉脉上面一句话: 始终保持自己的竞争力。所以,继续开写!

  一般的JavaScript源码看的已经没啥意思了,我也不会写什么xx入门新手教程,最终决定还是啃原来的硬骨头,从外层libuv => node => v8一步步实现原有的目标吧。

  libuv核心还是事件轮询,前几天从头到尾看了一遍官网的文档,对此有了一些更深的理解。

  (虽然现在开发用的mac,但是为了衔接前面的文章,所以代码仍旧以windows系统为基础,反正差别也不大)

  首先看一眼官网给的图:

  理论上轮询都是一个无尽循环,所以不用在意loop alive问题。

  上图中,udpate loop time、Run due timers两块内容我已经在别的博客中讲解过,这里就懒得发传送门了。

  有两个简单的概念需要稍微提一下,libuv中有两个抽象概念贯穿整个框架:handle、request。其中handle生命周期较长,且有自己回调方法的一个事务,比如说TCP的handle会处理每一个TCP连接,并触发connection事件。request属于handle中一个生命周期短,且简单的行为,比如向文件进行读、写等等。

  这一篇主要看一下接下来剩余的部分,由于性质不太一样,所以并不会按顺序依次分析,而是从易到难,且源码会做大量简化,有兴趣的人可以自己去看。

  事件轮询方法源码精炼如下:

int uv_run(uv_loop_t *loop, uv_run_mode mode) {
  // ...

  while (r != 0 && loop->stop_flag == 0) {
    // update loop time
    uv_update_time(loop);
    // run due timers
    uv__run_timers(loop);
    // call pending callbacks
    ran_pending = uv_process_reqs(loop);
    // run idle handles
    uv_idle_invoke(loop);
    // run prepare handles
    uv_prepare_invoke(loop);
    // poll的阻塞时间处理
    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);
    // poll for I/O
    if (pGetQueuedCompletionStatusEx)
      uv__poll(loop, timeout);
    else
      uv__poll_wine(loop, timeout);

    // run check handles
    uv_check_invoke(loop);
    // call close callbacks
    uv_process_endgames(loop);
  }
  // ...
  return r;
}

Call close callbacks

  这类回调比较特殊,官网是这么解释的: Close callbacks are called. If a handle was closed by calling uv_close() it will get the close callback called.

  简单来讲,就是仅在为了关闭一个handle,调用uv_close方法中所带的callback会被认为是一个close callbacks。在使用node的时候,所有的操作(比如fs.readFile)不可主动取消,所以轮询中这一步在JS层面是感知不到的。

  作用上相当于vue钩子函数中的destroy,由于触发是在轮询的最后一步,适合做一些收尾的工作,比如关闭文件描述符等等。

  源码中体现如下,首先是uv_close:

void uv_close(uv_handle_t* handle, uv_close_cb cb) {
    // 很多代码...

    case UV_PREPARE:
      uv_prepare_stop((uv_prepare_t*)handle);
      uv__handle_closing(handle);
      uv_want_endgame(loop, handle);
      return;
}

  uv_close方法除了做关闭handle的本职工作,在最后都会调用一个uv_want_endgame方法收尾,这个方法是一个静态方法。

INLINE static void uv_want_endgame(uv_loop_t* loop, uv_handle_t* handle) {
  if (!(handle->flags & UV_HANDLE_ENDGAME_QUEUED)) {
    handle->flags |= UV_HANDLE_ENDGAME_QUEUED;

    handle->endgame_next = loop->endgame_handles;
    loop->endgame_handles = handle;
  }
}

  内容十分简单,将handle插入到endgame_handles这个链表的表头。

  最后,只需要看一眼uv_process_endgames即可。

INLINE static void uv_process_endgames(uv_loop_t* loop) {
  uv_handle_t* handle;

  while (loop->endgame_handles) {
    handle = loop->endgame_handles;
    loop->endgame_handles = handle->endgame_next;

    handle->flags &= ~UV_HANDLE_ENDGAME_QUEUED;

    switch (handle->type) {
      case UV_TCP:
        uv_tcp_endgame(loop, (uv_tcp_t*) handle);
        break;
      // ...
    }
  }
}

  也很简洁明了,不停的取出endgame_handles链表中的handle,依次调用不同的callbacks即可。

Run idle hanldes、Run prepare handles、Run check handles

  这三个虽然名字不一样,但是主要作用类似,只是在调用顺序上有所不同。

  由于Poll for I/O是一个比较特殊的操作,所以这里提供prepare、check两个钩子函数可以在这个事务前后进行一些别的调用,大可以用vue的钩子函数created、mounted来帮助理解。

  idle除去调用较早,也影响poll for I/O这个操作的阻塞时间timeout,官网原文: If there are any idle handles active, the timeout is 0.正常情况下事件轮询会根据情况计算一个阻塞时间timout来决定poll for I/O操作的时间。

  这里用一个C++例子来证明调用顺序,忽略上面的宏,直接看main函数,特别简单!!!

#include <iostream>
#include "uv.h"
using namespace std;

void idle_callback(uv_idle_t* idle);
void prepare_callback(uv_prepare_t* prepare);
void check_callback(uv_check_t* check);

#define RUN_HANDLE(type) do {    uv_##type##_t type;    uv_##type##_init(loop, &type);    uv_##type##_start(&type, type##_callback);    } while(0)

#define CALLBACK(type)  do {    cout << "Run " << #type << " handles" << endl;   uv_##type##_stop(type);    } while(0)

#define OPEN(PATH, callback) do {    uv_fs_t req;    uv_fs_open(loop, &req, PATH, O_RDONLY, 0, callback); uv_fs_req_cleanup(&req);    } while(0)

void idle_callback(uv_idle_t* idle) { CALLBACK(idle); }
void prepare_callback(uv_prepare_t* prepare) { CALLBACK(prepare); }
void check_callback(uv_check_t* check) { CALLBACK(check); }
void on_open(uv_fs_t* req) { cout << "poll for I/O" << endl; }

int main(int argc, const char * argv[]) {
    auto loop = uv_default_loop();

    RUN_HANDLE(check);
    RUN_HANDLE(prepare);
    RUN_HANDLE(idle);

    OPEN("/Users/feilongpang/workspace/i.js", on_open);

    uv_run(loop, UV_RUN_DEFAULT);
    uv_loop_close(loop);
    return 0;
}

  执行的时候还发现了一个问题,如果不提供一个I/O操作,Run check handles那一步是会直接跳过,所以手动加了一个open操作。

  可以看到,我特意调整了callback的添加顺序,但是输出依然是:

  所以,代码确实是按照官网示例所给的图顺序来执行。

  剩下两个poll for I/O、pending callbacks留到下一篇讲吧。

原文地址:https://www.cnblogs.com/QH-Jimmy/p/10821342.html

时间: 2024-10-13 16:52:33

浅析libuv源码-node事件轮询解析(1)的相关文章

浅析libuv源码-node事件轮询解析(4)

这篇应该能结,简图如下. 上一篇讲到了uv__work_submit方法,接着写了. 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_on

浅析libuv源码-获取精确时间

在Timer模块中有提到,libuv控制着延迟事件的触发,那么必须想办法精确控制时间. 如果是JS,获取当前时间可以直接通过Date.now()得到一个时间戳,然后将两段时间戳相减得到时间差.一般情况下当然没有问题,但是这个方法并不保险,因为本地计算机时间可以修改. libuv显然不会用这么愚蠢的办法来计算时间,C++内部有更为精妙的方法来处理这个事. 首先在上一节中,一个简单的事件轮询代码如下: int main() { uv_loop_t *loop = uv_default_loop();

【译】理解node.js事件轮询

Node.js的第一个基本论点是I/O开销很大. 当前编程技术中等待I/O完成会浪费大量的时间.有几种方法可以处理这种性能上的影响: 同步:每次处理一个请求,依次处理.优点:简单:缺点:任何一个请求都可以阻塞所有其他的请求. Fork一个新进程:开一个新进程来处理每个请求.优点:容易:缺点:不能很好的扩展,成百上千个连接意味着成百上千个进程.fork()函数相当于Unix程序员的锤子,因为它很有用,每个问题看起来就像一个钉子,通常会被过度使用.(译者注:直译比较拗口,我理解的意思是,Unix程序

node.js事件轮询(1)

事件轮询(引用) 事件轮询是node的核心内容.一个系统(或者说一个程序)中必须至少包含一个大的循环结构(我称之为"泵"),它是维持系统持续运行的前提.nodejs中一样包含这样的结构,我们叫它"事件轮询",它存在于主线程中,负责不停地调用开发者编写的代码.我们可以查看nodejs官方网站上对nodejs的说明: Node is similar in design to and influenced by systems like Ruby's Event Mach

理解Node.js的事件轮询

前言 总括 : 原文地址:理解Node.js的事件轮询 Node小应用:Node-sample 智者阅读群书.亦阅历人生 正文 Node.js的两个基本概念 Node.js的第一个基本概念就是I/O操作开销是巨大的: 所以,当前变成技术中最大的浪费来自于等待I/O操作的完毕.有几种方法能够解决性能的影响: 同步方式:按次序一个一个的处理请求.利:简单.弊:不论什么一个请求都能够堵塞其它全部请求. 开启新进程:每一个请求都开启一个新进程.利:简单:弊:大量的链接意味着大量的进程. 开启新线程:每一

node.js的事件轮询机制

借助libuv库实现的 概括事件轮询机制:分为六个阶段1.timers 定时器阶段计时和执行到点的定时器回调函数 2.pending callbacks某些系统操作(例如TCP错误类型) 3.idle,prepare 4.poll轮询阶段(轮询队列)如果轮询队列不为空,依次同步取出轮询队列中第一个回调函数,直到轮询队列为空或者达到系统最大限制如果轮询队列为空 如果之前设置过setImmediate函数,直接进入下一个check阶段,如果之前没有设置过setImmediate函数,在当前 poll

libuv源码分析前言

Libevent,libev,libuv三者的区别所在? libevent提供了全套解决方案(事件库,非阻塞IO库,http库,DNS客户端),然而libevent使用全局变量,导致非线程安全.它的watcher结构也过大,把I/O.计时器.信号句柄整合在一起.而且(作者认为)libevent的附加组件如http和dns库都实现不好,且有安全问题. libev因libevent而诞生,对libevent做了改进,避免使用全局变量,拆分watcher等.另外libev去掉了外部库(比如http和d

事件轮询 event loop

Understanding the node.js event loop The first basic thesis of node.js is that I/O is expensive: So the largest waste with current programming technologies comes from waiting for I/O to complete. There are several ways in which one can deal with the

nodejs事件轮询详述

目录 概述 nodejs特点 事件轮询 关于异步方法 概述 关于nodejs的介绍网上资料非常多,最近由于在整理一些函数式编程的资料时,多次遇到nodejs有关的内容.所以就打算专门写一篇文章总结一下nodejs相关知识,包括“说它单线程是什么意思”.“非阻塞又是指什么”以及最重要的是它的“事件轮询”的实现机制. 本文不介绍nodejs的优缺点(适用场合).nodejs环境怎样搭建以及一些nodejs库的使用等等这些基础知识. nodejs特点 网上任何一篇关于nodejs的介绍中均会提及到no