我来回答饿了么大前端的问题(2)
发布于 7 年前 作者 TalkWIthKeyboard 7503 次浏览 来自 分享

事件/异步

Promise

  • promise迷你书

    • Promise对象的三个状态 has-resolution, has-rejection, unresolved
    • .then() 方法是异步调用的。
    var promise = new Promise(function (resolve){
           console.log("inner promise"); // 1
           resolve(42);
    });
    promise.then(function(value){
           console.log(value); // 3
    });
    console.log("outer promise"); // 2
    
    // 输出
    inner promise // 1
    outer promise // 2
    42            // 3
    
  • 其实在我的理解当中,同步是相对的。对一串连续的事件使用promise封装,他们之间是同步实行的,并不是对于外部。我们再来解释下这个例子:

    setTimeout(function() {
      console.log(1)
    }, 0);
    new Promise(function executor(resolve) {
      console.log(2);
      for( var i=0 ; i<10000 ; i++ ) {
        i == 9999 && resolve();
      }
      console.log(3);
    }).then(function() {
      console.log(4);
    });
    console.log(5);
    
    // 输出
    2
    3
    5
    4
    1
    

    这里会用到 Event Loop 的知识,后面有详细说明。首先代码顺序执行,注册了一个Timer事件,将Timer事件放入Timer队列中。然后有一个promise被放入 poll 队列中执行。先是有一个输出2的同步事件,被放入 poll 队列执行,再就出现了 resolve() ,一个异步事件先放入i/o callback队列中,再有一个输出3的同步事件,被放入 poll 队列执行。再是将输出5的同步事件放入 poll 执行,poll 空了以后,将i/o callback中的 resolve() 放入poll 执行,最后将Timer中的事件放入 poll 中执行。

Events

Events中的emit是同步的,会按照注册顺序来触发监听器。

一个事件监听器中监听同一个事件,会导致死循环?

const EventEmitter = require('events');

let emitter = new EventEmitter();

emitter.on('myEvent', () => {
  console.log('hi');
  emitter.emit('myEvent');
});

emitter.emit('myEvent');
  • 这种情况是会死循环的,其实就是无限的递归调用,运行一下果然崩栈了。 Maximum call stack size exceeded
const EventEmitter = require('events');

let emitter = new EventEmitter();

emitter.on('myEvent', function sth () {
  emitter.on('myEvent', sth);
  console.log('hi');
});

emitter.emit('myEvent');
  • 通过源码解析 Node.js 中 events 模块里的优化小细节
    • 这种情况不会出现死循环,因为在执行的过程当中是把原监听数组拷贝一份出来执行监听。当在监听器中监听同一个事件的时候,只会在原监听数组当中添加监听,而不会在这个拷贝后的数组添加。

Event Loop

  • JavaScript 运行机制详解:再谈Event Loop
    • 任务队列

      • 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
      • 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
      • 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
      • 主线程不断重复上面的第三步。
      • 所以异步任务永远是在同步任务之后开始执行的,不管他的代码位置如何。
      • 排在任务队列前面的事件优先进入执行栈,但是有些事件会被设置定时器,如果现在的时间小于了定时器的时间,那么会一直等到到达定时器的时间才会将事件加入执行栈。如果现在的时间大于了定时器的时间,也不会立即将它加入执行栈,会等到同步任务与它前面的异步任务执行完成以后再将它加入执行栈。
    • process.nextTick

      • 在当前"执行栈"的尾部,下一次Event Loop(主线程读取"任务队列")之前,触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。
      • 如果有多个 process.nextTick 语句(不管它们是否嵌套),将全部在当前"执行栈"执行。
    • setImmediate

      • 在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与 setTimeout(fn, 0) 很像。
      • process.nextTicksetImmediate 的一个重要区别:多个 process.nextTick 语句总是在当前"执行栈"一次执行完,多个setImmediate可能则需要多次loop才能执行完。事实上,这正是Node.js 10.0版添加 setImmediate 方法的原因,否则像下面这样的递归调用 process.nextTick,将会没完没了,主线程根本不会去读取"事件队列"!
  • Node.js Event Loop 的理解 Timers,process.nextTick()
    • 这一篇对于 Event Loop 的分析就更加进阶了,我们首先看 Event Loop 的各个阶段。

         ┌───────────────────────┐
      ┌─>│        timers         │
      │  └──────────┬────────────┘
      │  ┌──────────┴────────────┐
      │  │     I/O callbacks     │
      │  └──────────┬────────────┘
      │  ┌──────────┴────────────┐
      │  │     idle, prepare     │
      │  └──────────┬────────────┘      ┌───────────────┐
      │  ┌──────────┴────────────┐      │   incoming:   │
      │  │         poll          │<─────┤  connections, │
      │  └──────────┬────────────┘      │   data, etc.  │
      │  ┌──────────┴────────────┐      └───────────────┘
      │  │        check          │
      │  └──────────┬────────────┘
      │  ┌──────────┴────────────┐
      └──┤    close callbacks    │
         └───────────────────────┘
      
      • timers: 执行各种定时器预约的操作, setTimeout(callback)setInterval(callback)
      • I/O callbacks: 执行除了 close 事件的callback和属于 timerscallback以外的callback事件。
      • idle, prepare: 仅node内部使用。
      • poll: 获取新的I/O事件, 适当的条件下node将阻塞在这里;(在我的理解中,这个阶段才是一个 Event Loop 的开始)
      • check: 执行 setImmediate() 设定的callbacks;
      • close callbacks: close 事件的回调会在该阶段执行。
      • 而还有一个独立于 Event Loop 外的过程就是 process.nextTick() 它是在各个阶段的切换阶段进行调用。
    • Poll阶段

      • 代码中没有设置 timer
        • poll quenue 不为空时,同步执行队列中的所有事件。直到队列为空,或执行的callback达到系统的上限。
        • poll quenue 为空时,如果设定了 setImmediate() 则进入 check阶段,没有设定的话,就阻塞等待callback进入队列。
      • 代码中设置 timer
        • poll queue进入空状态时,event loop 将检查timers,如果有1个或多个timers时间时间已经到达,event loop 将按循环顺序进入timers阶段,并执行timer queue.

进程

Process

  • process.cwd() :返回运行当前脚本的工作目录的路径。
  • process.chdir() :改变工作目录。

process.nextTick

这个在上面已经分析过了,不赘述。

标准流

  • console.log 的实现

    exports.log = function() {
        process.stdout.write(format.apply(this, arguments) + '\n');
    };
    

    通过实现来说,我觉得 console.log 是同步的。

  • 同步输入的实现 (nodeJS 中从命令行等待并读入用户输入实现与用户交互的方法):

    • 主要是思路是将 fs.readSync 的输入流从定向到 process.stdin.fd
    • 我觉得还有一种方法是对输入流进行监听
  • Linux 中关于进程管理的命令

    • topTOP 是一个动态显示过程,即可以通过用户按键来不断刷新当前状态.如果在前台执行该命令,它将独占前台,直到用户终止该程序为止.比较准确的说, top 命令提供了实时的对系统处理器的状态监视.它将显示系统中CPU最“敏感”的任务列表.该命令可以按CPU使用.内存使用和执行时间对任务进行排序;而且该命令的很多特性都可以通过交互式命令或者在个人定制文件中进行设定.
    • psps 命令就是最基本同时也是非常强大的进程查看命令.使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵尸、哪些进程占用了过多的资源等等。总之大部分信息都是可以通过执行该命令得到的。
    • pstree: Linux pstree命令将所有行程以树状图显示,树状图将会以 pid (如果有指定) 或是以 init 这个基本行程为根 (root),如果有指定使用者 id,则树状图会只显示该使用者所拥有的行程。

Child Process

####child_process.forkPOSIXfork 有什么区别?

  • POSIXfork每天进步一点点——论fork()函数与Linux中的多线程编程
    • 当程序调用 fork() 函数并返回成功之后,程序就将变成两个进程,调用 fork() 者为父进程,后来生成者为子进程。这两个进程将执行相同的程序文本,但却各自拥有不同的栈段、数据段以及堆栈拷贝。子进程的栈、数据以及栈段开始时是父进程内存相应各部分的完全拷贝,因此它们互不影响。如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。
  • 但是现在没有找到 child_process.fork 如果不是对当前父进程的拷贝,那他的具体实现原理是什么。

child.killchild.send 的区别

  • 在谈这两者的区别的时候是需要先谈一下 forkspawn 的区别:NODEJS硬实战笔记(多进程)
    • 实例化一个 spawn 后,会返回一个 ChildProcess 对象,该对象中包含了 stdinstdoutstderr 流对象。而实例化一个 fork ,默认是会继承父进程的stdinstdoutstderr 流(当然也可以打开),并且会单独建立一个IPC通道,使得父子进程之间可以通过监听 message 事件来进行通信,所以 fork 实际上是 spawn 的一个特例。
      • send 是需要依赖 IPC 通道进行通信的,所以只有通过 fork 的子进程才能与父进程之间使用 send 通信的。kill 是通过信号系统来进行通信,只要操作系统支持信号系统就行。

孤儿进程与僵尸进程

子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。

  • **孤儿进程:**一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。 spawn 中可以通过 options.detached 指定父进程死亡后是否允许子进程存活。 + 孤儿进程在被init进程接手以后,init进程会循环地 wait() 它已经退出的子进程,来进行善后,所以孤儿进程不会有什么危害。
  • 僵尸进程:一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 waitwaitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。 + 僵尸进程因为没有被父进程调用 wait ,进程号、退出状态、运行时间等都被保存在内存中,特别是进程号一直被占用着,而系统的进程号又是有限的,所以大量的僵尸进程是会带来非常大的威胁的。

Cluster

round-robin

其实就是找下一个空闲的 worker,但是我们可以看看 Cluster 的进化过程,从最开始的自由竞争(将引起 惊群效率),将每个连接放到 worker 竞争到由 master 获取连接再进行分配。

进程间通信

Linux进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)

  • 从原理上,管道利用fork机制建立,从而让两个进程可以连接到同一个PIPE上。最开始的时候,读入流和输出流都连接在同一个进程Process 1上。当fork复制进程的时候,会将这两个连接也复制到新的进程(Process 2)。随后,每个进程关闭自己不需要的一个连接 (一个关闭读入流,一个关闭输出流),这样,剩下的连接就构成了PIPE。
  • 由于管道只能在父子进程之间进行通信,为了解决这一问题就提供了FIFO方法连接进程,**FIFO (First in, First out)为一种特殊的文件类型,它在文件系统中有对应的路径。当一个进程以读®的方式打开该文件,而另一个进程以写(w)**的方式打开该文件,那么内核就会在这两个进程之间建立管道。FIFO的好处在于我们可以通过文件的路径来识别管道,从而让没有亲缘关系的进程之间建立连接。
  • 信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。
    • 内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。
    • 内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。

守护进程

通过系统命令:Linux 守护进程的启动方法

  • 首先要明确,通过使用 & 来启动后台任务的时候,实际上是不再继承当前session的 stdin ,会继续继承 stdoutstderr
  • 然后需要明确当用户退出当前session的时候,系统会向session发出 SIGHUP 信号,session再将该信号转发给所有子进程,子进程收到后就会退出,所以普通的后台进程是不能实现守护进程的需求的。
  • 那么实现的三个思路是:**1. 不要让session将 SIGHUP 信号转发给后台任务;2.将守护任务从后台任务列表当中移出。但是光是这样还是不行,因为后台任务还是在继承该session的 stdoutstderr,所以还需要将这两个流重定向到外面。**那么第三个思路是与前两个截然不同的,第三个是重建session,将后台任务启动到这个新进程当中。

通过node:Nodejs编写守护进程

  • 创建一个进程A。
  • 在进程A中创建进程B,我们可以使用fork方式,或者其他方法。
  • 对进程B执行 setsid 方法。
    • Linux进程组和会话
    • 该进程变成一个新会话的会话领导。
    • 该进程变成一个新进程组的组长。
    • 该进程没有控制终端。
  • 进程A退出,进程B由init进程接管。此时进程B为守护进程。
回到顶部