zoukankan      html  css  js  c++  java
  • nodejs中异步

    nodejs中的异步

    1 nodejs 中的异步存在吗?

    现在有点 javascript 基础的人都在听说过 nodejs ,而只要与 javascript 打交到人都会用或者是将要使用 nodejs 。毕竟 nodejs 的生态很强大,与 javascript 相关的工具也做的很方便,很好用。

    javascript 语言很小巧,但是一旦与 nodejs 中的运行环境放在一起,有些概念就很难理解,特别是异步的概念。有人会说不会啊,很好理解啊?不就是一个ajax请求加上一个回调函数,这个ajax函数就是能异步执行的函数,他在执行完了就会调用回调函数,我承认这个样做是很容易,早些时候我甚至认为在 javascript 中加了回调函数的函数都可以异步的,异步和回调函数成对出现。多么荒谬的理解啊!

    直到有一天,我在写程序时想到一个问题:在 nodejs 中在不调用系统相关 I/O ,不调用 c++ 写的 plugin 的情况下,写一个异步函数?我查了资料,有人给我的答案是调用 setTimeout(fn,delay) 就变成了异步了。但是我还是不明白为什么要调用这样一个函数,这个函数的语义跟async完全不一样,为什么这样就行?

    带着这个疑问,我查了很多资料,包括官方文档,代码,别人的blog。慢慢的理解,最后好像是知道了为什么会是这样,整篇文章就是对所了解东西的理解。恳请大家批评指正。

    说明:nodejs 的文档是用的 v5.10.1 API,而代码方面:nodejs 和 libuv 是用的 master 分支。

    2 nodejs 的架构基础

    2.1 基础

    在探索 nodejs 的异步时,首先需要对 nodejs 架构达成统一认识:

    1. nodejs 有 javascript 的运行环境,目前它的实现是 chrome 的 V8 引擎。
    2. nodejs 基于事件驱动和非阻塞 I/O 模型,目前它的实现是 libuv。
    3. 当前的 libuv 是多线程的,文档中有说明。
    4. nodejs 在运行时只生成了一个 javascript 运行环境 的实例,也就是说 javascript 解释器只有一个。
    5. nodejs 在主线程中调用 V8 引擎的实例执行 javascript 代码。

    如果以上 5 点你不认同的话,那下面就不需要看了,看了会觉得漏洞百出。

    上面的 5 点主要说明另一层意思了:

    1. nodejs 的 javascript 运行环境可以换,在 nodejs 官方 github
      中 PR,可以看这个,微软想把 javascript 运行环境换成自己家的。
    2. nodejs 的事件驱动和非阻塞 I/O 模型也可以换,目前来看 libuv 运行的不错,大家都很高兴。另外,你可能不知道,chromium 和 chrome 中使用了另一个实现 libevent2,证据在这里:链接
    3. nodejs 不是单线程,它是多线程程序,因为 libuv 就已经是多线程了。官方文档在这里 cli_uv_threadpool_size_size
    4. 因为是嵌入式 js 引擎,只能调用宿主环境中提供的方法。当前来说,nodejs 主要把 libuv 的 io/timer 接口提供给了 js 引擎,其他的没有提供(包括 libuv 的工作线程)。
    5. nodejs 也没有提供给 js引擎 新建调用系统线程的任何方法,所以在nodejs中执行 javascript,是没有办法新开线程的。
    6. js 引擎只有一个实例且在 nodejs 的主线程中调用。

    2.2 结论

    1. nodejs 中存在异步,集中在 I/O 和 Timer 调用这一块,其他的地方没有。
    2. js 引擎没有异步或者并行执行可能,因为 js 引擎是在 nodejs 的主线程调用,所以 js 引擎执行的 javascript 代码都是同步执行,没有异步执行。所以你想写出来一个不调用 I/O和 的异步方法,不可能。

    那nodejs中常谈的异步回调是怎么回事?

    3 nodejs 中的回调和异步的关系是什么?

    3.1 为什么是回调

    在 javascript 中使用回调函数可所谓登峰造极,基本上所有的异步函数都会要求有一个回调函数,以至于写 javascript 写多了,看到回调函数的接口,都以为是异步的调用。

    但是真相是回调函数,只是javascript 用来解决异步函数调用如何处理返回值这个问题的方法,或这样来说:异步函数调用如何处理返回值这个问题上,在系统的设计方面而言,有很多办法,而 nodejs 选择了 javascript 的传统方案,用回调函数来解决这个问题

    这个选择好不好,我认为在当时来说,很合适。但随着 javascript 被用来写越来越大的程序,这个选择不是一个好的选择,因为回调函数嵌套多了真的很难受,我觉得主要是很难看,(就跟 lisp 的 )))))))))))) ),让一般人不好接受,现在情况改善多了,因为有了Promise。

    3.2 结论

    1. 回调函数与异步没有关系,只是在 javascript 中用来解决异步的返回值的问题,所以异步函数必须带一个回调函数,他们成对出现,让人以为有关系。
    2. 在 javascript 中有回调不一定是异步函数,而异步必须带一个回调函数。

    4 nodejs 中怎样解决异步的问题?

    前面也说了,nodejs 的 js 引擎不能异步执行 javascript 代码。那js中我们常使用的异步是什么意思的?

    答案分为两部分:

    第一部分:与I/O和timer相关的任务,js引擎确实是异步,调用时委托 libuv 进行 I/O 和timer 的相关调用,好了之后就通知 nodejs,nodejs 然后调用 js 引擎执行 javascript 代码;

    第二部分:其它部分的任务,js 引擎把异步概念(该任务我委托别人执行,我接着执行下面的任务,别人执行完该任务后通知我)弱化成稍后执行(该任务我委托自己执行但不是现在,我接着执行下面的任务,该任务我稍后会自己执行,执行完成后通知我自己)的概念。

    这就是 js 引擎中异步的全部意思。基本上等同我们常说的:我马上做这件事。不过还是要近一步解释一下第二部分:

    1. 任务不能委托给别人,都是自己做。
    2. 如果当前我做的事件需要很长时间,那我马上要做的事一直推迟,等我做了完手头这件事再说。

    nodejs 中 js 引擎把异步变成了稍后执行,使写 javascript 程序看起来像异步执行,但是并没有减少任务,因此在 javascript 中你不能写一个需要很长时间计算的函数(计算Pi值1000位,大型的矩阵计算),或者在一个tick(后面会说)中执行过多的任务,如果你这样写了,整个主线程就没有办法响应别的请求,反映出来的情况就是程序卡了,当然如果非要写当然也有办法,需要一些技巧来实现。

    而 js 引擎稍后执行稍后到底是多久,到底执行哪些任务?这些问题就与 nodejs 中四个重要的与时间有关的函数有关了,他们分别是:setTimeout,setInterval,process.nextTick,setImmediate。下面简单了解一下这四个函数:

    4.1 setTimeout 和 setInterval

    setImeout 主要是延迟执行函数,其中有一个比较特别的调用:setTimeout(function(){/* code */},0),经常见使用,为什么这样使用看看后面。还有 setInterval 周期性调用一个函数。

    4.2 setImmediate 和 process.nextTick

    setImmediate 的意思翻译过来是立刻调用的意思,但是官方文档的解释是:

    Schedules "immediate" execution of callback after I/O events' callbacks and before timers set by setTimeout and setInterval are triggered.

    翻译过来大意就是:被 setImmediate 的设置过的函数,他的执行是在 I/O 事件的回调执行之后,在 计时器触发的回调执行之前,也就是说在 setTimeout 和 setInterval 之前,好吧这里还有一个顺序之分。

    process.nextTick 可就更怪了。官方的意思是:

    It runs before any additional I/O events (including timers) fire in subsequent ticks of the event loop.

    翻译过来大意就是:他运行在任何的 I/O 和定时器的 subsequent ticks 之前。

    又多了很多的概念,不过别慌,在下面会讲 nodejs 的EventLoop,这里讲的很多的不理解地方就会在 EventLoop 中讲明白。

    5 nodejs 中神秘的 EventLoop

    EvevtLoop大体上来说就是一个循环,它不停的检查注册到他的事件有没有发生,如果发生了,就执行某些功能,一次循环通常叫tick。这里有讲EventLoop,还有这里

    在 nodejs 中也存在这样一个 EventLoop,不过它是在 libuv 中。它每一次循环叫 tick。而在每一次 tick 中会有不同的阶段,每一个阶段可以叫 subTick,也就说是这个tick的子tick,libuv就有很多的子 tick,如I/O 和定时器等。下面我用一张图来表示一下,注意该循环一直在 nodejs 的主线程中运行:

        +-------------+
        |             |
        |             |
        |       +-----v----------------------+
        |       |                            |
        |       | uv__update_time(loop)      |  subTick
        |       |                            |
        |       +-----+----------------------+
        |             |
        |             |
        |       +-----v----------------------+
        |       |                            |
        |       | uv__run_timers(loop)       |  subTick
        |       |                            |
    tick|       +-----+----------------------+
        |             |
        |             |
        |       +-----v----------------------+
        |       |                            |
        |       | uv__io_poll(loop, timeout) |  subTick
        |       |                            |
        |       +-----+----------------------+
        |             |
        |             |
        |       +-----v----------------------+
        |       |                            |
        |       | uv__run_check(loop)        |  subTick
        |       |                            |
        |       +-----+----------------------+
        |             |
        |             |
        |             |
        +-------------+
    

    以上的流程图已经进行了裁减,只保留重要的内容,如果你想详细了解,可在 libuv/src/unix/core.cc,第334行:uv_run函数进行详细了解。

    下面来解释一下各个阶段的作用:

    uv__update_time是用来更新定时器的时间。uv__run_timers是用来触发定时器,并执行相关函数的地方。uv__io_poll是用来 I/O触发后执行相关函数的地方。
    uv__run_check的用处代码中讲到。

    了解到 nodejs 中 EventLoop 的执行阶段后,需要更深一步了解在 nodejs 中 js引擎和EvevtLoop是如何被整合在一起工作的。以下是一些伪代码,它用来说明一些机制。

    不过你需要知道在 nodejs 中 setTimeout、setInterval、setImmediate和process.nextTick都是系统级的调用,也就是他们都是c++ 来实现的。setTimeout和setInterval 可看看这个文件:timer_wrap.cc。另外两个我再补吧。

    class V8Engine {
      let _jsVM;
      
      V8Engine(){
         _jsVM = /*js 执行引擎 */;
      }
      
      void invoke(handlers){
      // 依次执行,直到 handlers 为空
        handlers.forEach(handler,fun => _jsVM.run(handler));
      }
    }
    
    class EvenLoop {
      let _jsRuntime = null;
      let _callbackHandlers = []; 【1】
      let _processTickHandlers = []; 【2】
      let _immediateHandlers = []; 【3】
    
      // 构造函数
      EvenLoop(jsRuntime){
       _jsRuntime = jsRuntime;
      }
    
      void start(){
        where(true){
          _jsRuntime.invoke(_processTickHandlers); 【4】
          _processTickHandlers.clear();
    
          update_time();
          run_timer(); 
          run_pool();
          run_check();
    
          if (process.exit){
            _jsRuntime.invoke(_processTickHandlers); 【5】
            _processTickHandlers.clear();
            break;
          }
        }
      }
    
      void update_time(){
          //  更新 timer 的时间
      }
    
      void run_timer(){ 【6】
        let handlers = getTimerHandler(); 
        _callbackHandlers.push(handlers);
        _jsRuntime.invoke(_callbackHandlers);
        _jsRuntime.invoke(_processTickHandlers);
        _callbackHandlers.clear();
        _processTickHandlers.clear();
      }
    
      void run_pool(){  【6】
        let handlers = getIOHandler(); 
        _callbackHandlers.push(handlers);
        _jsRuntime.invoke(_callbackHandlers);
        _jsRuntime.invoke(_processTickHandlers);
        _callbackHandlers.clear();
        _processTickHandlers.clear();
      }
    
      void run_check(){  【7】
        let handlers = getImmediateHandler();
        _immediateHandlers.push(handlers);
        _jsRuntime.invoke(_immediateHandlers);
        _immediateHandlers.clear();
      }
     
    }
    
    main(){
      JsRuntime jsRuntime = new V8Engine();
      EventLoop eventLoop = new EventLoop(jsRuntime);
      eventLoop.start();
    }
    
    // 主线程中执行
    main();
    

    以上代码是 nodejs 的粗略的执行过程,还想进一步了解,可以看这从入口函数看起:node_main.cc

    按标号进行说明:

    1. 全局的回调事件先进先出队列,包括了 I/O 事件和 Timer 事件的回调对象。
    2. 全局的nextTick的回调对象先进先出队列。
    3. 全局的setImmediate的回调对象先进先出队列。
    4. 开始时会执行 nextTick的队列。
    5. 程序退出时会执行 nextTick的队列。
    6. 可以看出nextTick队列会在run_timerrun_pool之后执行。回到第三节说的nextTick的执行时机,看出来该队列确实会在 I/O 和 Timer 之前运行。在文档中特别说明如果你递归调用 nextTick 会阻 I/O 事件的调用就像调用了 loop。依照上面的伪代码,发现如果你递归调用nextTick,那nextTick回调对象先进先出队列就不会为空,js 引擎就一直在执行,影响之后的代码执行。
    7. setImmediate 回调对象先进先出队列,每一次 tick 就执行一次。

    可以从代码中看出这四个时间函数执行时机的区别,而setTimeout(fn,0)是在 _callbackHandlers的队列中,而setImmediate,还有 nextTick 都在不同的队列中执行。

    总体来说,nextTick执行最快,而setTmmediate能保证每次tick都执行,而setTimeout是 libuv 的 Timber 保证,可能会有所延迟。

    相关链接

    1. 有人觉得得 process.nextTick 名不副实,得改个名字,变成 process.currentTick,没有通过,理由是太多的代码依赖这个函数了,没有办法改名字,这里
    2. 如果你觉得 EventLoop 我说的不清楚,你还可以看看这篇博客:链接
    3. 如果你觉得 setImmediate 和 nextTick 说的不清楚,可以看这:链接
    4. 这个也可以:链接
    5. Synchronously asynchronous
    6. designing-apis-for-asynchrony
    7. ***** javaScript 运行机制详解:再谈Event Loop 的博客真的很容易懂
    8. nodejs真的是单线程吗?,这篇文章讲的不错。

    6 nodejs 回调和大数据与大计算量的解决方案

    6.1 回调解决方案- promise

    我相信你一但用了promise,你就回不去以往的回调时代,promise 非常好使用,强列推荐使用。如果你还想了解promise怎么实现的,我给你透个底,必不可少setTimeout这个函数,可以参考 Q promise的设计文档,还有一步步来手写一个Promise也不错。

    6.2 大数据与大计算量的解决方案 - 分片数据或者分片计算

    如果要写一个处理数据量很大的任务,我想这个函数可以给你思路:

    6.2.1 yielding processes

    function chunk(array,process,context){
      setTimeout(function(){
        var item = array.shift();
        process.call(context,item);
    
        if (array.length >0){
          setTimeout(arguments.callee,100);
        }
      },100)
    }
    

    6.2.2 函数节流

    如果要写一个计算量很大的任务,这个函数也可以给你思路:

    var process = {
      timeout = null,
    
      // 实际进行处理的方法
      performProcessing:function(){
        // 实际执行的代码
      },
    
      // 初始处理调用的方法
      process:function(){
        clearTimeout(this.timeoutId);
    
        var that = this;
        this.timeoutId = setTimeout(function(){
          that.performProcessing();
        },100)
      }
    }
    

    这两个函数是从JavaScript高级程序设计第612-615页摘出来的,本质是不要阻塞了Javascript的事件循环,把任务分片了。

    6.3 负载

    做服务器请求多了,使用 cluster 模块。cluster 的方案就是nodejs的多进程方案。cluster 能保证每个请求被一个 nodejs 实例处理。这样就能减少每个 nodejs 的处理的数据量。

    7 总结

    从现在来看 nodejs 架构中对 js 引擎不支持线程调用是一个较大的遗憾,意味着在 nodejs 中你甚至不能做一个很大的计算量的事。不过又说回来,这也是一件好事。因为这样做的,使 javascript 变简单,写 js 不需要考虑锁的事情,想想在 java 中集合类加锁,你还要考虑同步,你还要考虑死锁,我觉得写 js 的人都很幸福。

    7.1 其他语言

    同样的问题也出现在 python、ruby 和 php 上。这些语言在当前的主流版本(用c实现的版本)中都默认一把大锁 GIL,所有的代码都是主线程中运行,代码都是线程安全的,基本上第三方库也利用这个现实。导致的事实是它们都没有办法很好的利用现在的多核计算机,多么悲剧的事情啊!

    不过好在,计算这事情,它们干不了,还有人来干,就是老大哥 c、c++还有 java 了。你没有看到分布式计算领域和大数据中核心计算被老大哥占领,其他是想占也占不了,不是不想占,是有心无力。

    就目前的分析,我觉得这篇文章说的很对。

    7.2 未来发展

    当前 nodejs 的发展还是在填别的语言中经历过的坑,因为 nodejs 发展毕竟才七年的时间(2009年建立),流行也才是近几年的事情。不过 nodejs 的进步很快(后发优势),做一个轻量级的网页应用已经是继 python、ruby、php之后的另一个选择了,可喜可贺。

    但是如果还要更近一步发展,那就必须解决计算这个问题。当前 javascript 对于这个问题的解决基本还是按着沿用 python、ruby 和 php 走过的路线走下去,采用单线程协程的方案,也就是 yieldasync/wait 方案。在这之后,也基本上会采用多线程方案 worker 。从这样的发展来看,未来的 nodejs 与 python、ruby、php 是并驾齐驱的解决方案,不见得比 python、ruby 和 php 更好,它们都差不多,唯一不同的是我们又多了一种选择而已。

    想到程序员在论坛上问:新手学习网站开发,javacript、python、ruby和 php 哪个好?我想说如果有师博他说什么好就学什么,如果没有师博那就学 javascript 吧,因为你不用再去学一门后端的语言了。

  • 相关阅读:
    第12章 Swing编程
    第11章 AWT编程
    第10章 异常处理
    第9章 泛型
    Java 实例
    Spring 框架 (持续完善中)
    Java 程序员必备的5个框架 (持续完善中)
    IDEA 中建立Java项目步骤
    Java 实例
    Java 实例
  • 原文地址:https://www.cnblogs.com/htoooth/p/5406831.html
Copyright © 2011-2022 走看看