Event|Event Loop

关于线程和进程

  • 核心理论:
    • CPU: 计算机的核心是,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
    • 单个CPU一次只能运行一个任务
    • 进程(Process):操作系统分配资源和调度任务的基本单位,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
    • 线程(Thread):建立在进程上的一次程序运行单位, 多个线程组合成一个进程任务
    • 基本理论
      • 多进程形式允许多个任务同时运行
      • 多线程形式,允许单个任务分成不同的部分运行;
      • 提供协调机制(一个是互斥锁 Mutex,一个是信号量),一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源
多线程 跟 多进程的部分场景下的优劣选择
对比维度 多进程 多线程 总结
数据共享、同步 数据共享复杂,需要用IPC;数据是分开的,同步简单 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 各有优势
内存、CPU 占用内存多,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高 线程占优
创建销毁、切换 创建销毁、切换复杂,速度慢 创建销毁、切换简单,速度很快 线程占优
编程、调试 编程简单,调试简单 编程复杂,调试复杂 进程占优
可靠性 进程间不会互相影响 一个线程挂掉将导致整个进程挂掉 进程占优
分布式 适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 适应于多核分布式 进程占优
Event Loop
  • 【Event|Event Loop】堆(heap)
    堆(heap)
    是指程序运行时申请的动态内存,在JS运行时用来存放对象。
  • 栈(stack)
    栈(stack)遵循的原则是“先进后出”
栈内存1 存放 JS中的基本数据类型 与 指向对象的地址 栈内存2(简称:执行栈) 执行 JS主线程

  • 队列(queue)
    队列(queue)遵循的原则是“先进先出”
    JS中除了主线程之外还存在一个“任务队列”(其实有两个,后面再详细说明)。

浏览器中的Event Loop
Event|Event Loop
文章图片
image.png 详细流程描述
  • 所有同步任务都在主线程上执行,形成一个执行栈和堆
  • 主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,将队列中的事件放到执行栈中依次执行
  • 主线程从任务队列中读取事件,这个过程是循环不断的
case 如下:
例如利用setTimeout 实现的这个case
console.log(1); console.log(2); setTimeout(function(){ console.log(3) setTimeout(function(){ console.log(6); }) },0) setTimeout(function(){ console.log(4); setTimeout(function(){ console.log(7); }) },0) console.log(5)

代码中的setTimeout的时间给得0,相当于4ms,也有可能大于4ms(不重要)。我们要注意的是代码输出的顺序。我们把任务以其输出的数字命名。
先执行的一定是同步代码,先输出1,2,5,而3任务,4任务这时会依次进入“任务队列中”。同步代码执行完毕,队列中的3会进入执行栈执行,4到了队列的最前端,3执行完后,内部的setTimeout将6的任务放入队列尾部。开始执行4任务……
最终我们得到的输出为1,2,5,3,4,6,7。
处理步骤为:
  • 主线程执行,形成执行栈
  • 执行栈优先执行同步代码,优先输出1,2,5
  • 异步任务 推到主线程之外的任务队列(callback queue)
  • 同步代码执行完毕, 任务队列中的3会进入执行栈执行, 4到了队列的最前端
  • 3执行完后,内部的setTimeout将6的任务放入队列尾部。开始执行4任务
  • 依次执行.
Node 中的 Event Loop
术语阐述
  • 事件驱动(event-driven)
    是nodejs中的第二大特性。何为事件驱动呢?
    简单来说,就是通过监听事件的状态变化来做出相应的操作
    比如读取一个文件,文件读取完毕,或者文件读取错误,那么就触发对应的状态,然后调用对应的回掉函数来进行处理。
  • 线程驱动
    是当收到一个请求的时候,将会为该请求开一个新的线程来处理请求
    一般存在一个线程池,线程池中有空闲的线程,会从线程池中拿取线程来进行处理,如果线程池中没有空闲的线程,新来的请求将会进入队列排队,直到线程池中空闲线程
  • nodejs 单线程(single thread)
    nodejs是单线程运行的,通过一个事件循环(event-loop)来循环取出消息队列(event-queue)中的消息进行处理,处理过程基本上就是去调用该消息对应的回调函数。消息队列就是当一个事件状态发生变化时,就将一个消息压入队列中。

nodejs的时间驱动模型一般要注意下面几个点:
  1. 因为是单线程的,所以当顺序执行js文件中的代码的时候,事件循环是被暂停的。
  2. 当js文件执行完以后,事件循环开始运行,并从消息队列中取出消息,开始执行回调函数
  3. 因为是单线程的,所以当回调函数被执行的时候,事件循环是被暂停的
  4. 当涉及到I/O操作的时候,nodejs会开一个独立的线程来进行异步I/O操作,操作结束以后将消息压入消息队列。
实现异步IO
异步IO(asynchronous I/O) 首先来理解几个容易混淆的概念,阻塞IO(blocking I/O)和非阻塞IO(non-blocking I/O),同步IO(synchronous I/O)和异步IO(synchronous I/O)。
阻塞I/O 和 非阻塞I/O 简单来说,阻塞I/O
当用户发一个读取文件描述符的操作的时候,进程就会被阻塞,直到要读取的数据全部准备好返回给用户,这时候进程才会解除block的状态。
那非阻塞I/O呢,就与上面的情况相反
当用户发起一个读取文件描述符操作的时,函数立即返回,不作任何等待,进程继续执行。但是程序如何知道要读取的数据已经准备好了呢?最简单的方法就是轮询。
除此之外,还有一种叫做IO多路复用的模式,就是用一个阻塞函数同时监听多个文件描述符,当其中有一个文件描述符准备好了,就马上返回,在linux下,select,poll,epoll都提供了IO多路复用的功能。
同步I/O 和 异步I/O 那么同步I/O和异步I/O又有什么区别么?
是不是只要做到非阻塞IO就可以实现异步I/O呢?
其实不然。
同步I/O(synchronous I/O)做I/O operation的时候会将process阻塞,所以阻塞I/O,非阻塞I/O,IO多路复用I/O都是同步I/O。
异步I/O(asynchronous I/O)做I/O opertaion的时候将不会造成任何的阻塞。
非阻塞I/O都不阻塞了为什么不是异步I/O呢?其实当非阻塞I/O准备好数据以后还是要阻塞住进程去内核拿数据的。所以算不上异步I/O。
宏任务与微任务 任务队列中的所有任务都是会乖乖排队的吗?
答案是否定的,任务也是有区别的,总是有任务会有一些特权(比如插队),就是任务中的vip--微任务(micro-task),那些没有特权的--宏任务(macro-task)。
我们看一段代码:
console.log(1); setTimeout(function(){ console.log(2); Promise.resolve(1).then(function(){ console.log('promise') }) })setTimeout(function(){ console.log(3); })

按照“队列理论”,结果应该为1,2,3,promise。
可是实际结果事与愿违输出的是1,2,promise,3。
明明是3先进入的队列 ,为什么promise会排在前面输出?
这是因为promise有特权是微任务,当主线程任务执行完毕微任务会排在宏任务前面先去执行,不管是不是后来的。
换句话说,就是任务队列实际上有两个,一个是宏任务队列,一个是微任务队列,
当主线程执行完毕,如果微任务队列中有微任务,则会先进入执行栈,
当微任务队列没有任务时,才会执行宏任务的队列。
微任务,如下:
原生Promise(有些实现的promise将then方法放到了宏任务中) Object.observe(已废弃) MutationObserver MessageChannel 等

宏任务, 如下:
setTimeout setInterval setImmediate I/O 等

执行图示
┌───────────────────────┐ ┌─>│timers│ │└──────────┬────────────┘ │┌──────────┴────────────┐ ││I/O callbacks│ │└──────────┬────────────┘ │┌──────────┴────────────┐ ││idle, prepare│ │└──────────┬────────────┘┌───────────────┐ │┌──────────┴────────────┐│incoming:│ ││poll│<─────┤connections, │ │└──────────┬────────────┘│data, etc.│ │┌──────────┴────────────┐└───────────────┘ ││check│ │└──────────┬────────────┘ │┌──────────┴────────────┐ └──┤close callbacks│ └───────────────────────┘

Event|Event Loop
文章图片
image.png 流程描述
  • js代码会交给 v8引擎 进行处理
  • 代码中可能会调用nodeApi,node会交给 libuv库 处理
  • libuv通过 阻塞i/o多线程 实现了异步io
  • 通过事件驱动的方式,将结果放到事件队列中,最终交给我们的应用。
process.nextTick
process.nextTick方法不在上面的事件环中,我们可以把它理解为微任务,
它的执行时机是当前"执行栈"的尾部
---- 下一次Event Loop(主线程读取"任务队列")之前
---- 触发回调函数。
也就是说,它指定的任务总是发生在所有异步任务之前。
setImmediate方法则是在当前"任务队列"的尾部添加事件,
也就是说,它指定的任务总是在下一次 Event Loop 时执行
上代码:
process.nextTick(function A() { console.log(1); process.nextTick(function B(){ console.log(2); }); }); setTimeout(function timeout() { console.log('TIMEOUT FIRED'); }, 0); // 1 // 2 // TIMEOUT FIRED

代码可以看出,不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。
这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。

    推荐阅读