js "多线程" 与 异步调用 EventLoop 机制

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

当然也并不是多线程就是好的, 多线程虽然可以共享全局变量, 但是很容易造成问题, 因此需要原子锁, 互斥锁等保证一个变量不能被多个线程同时访问, js引擎是单线程运行的可能跟他的设计初衷也是有关的, 他并不需要那么多线程, 浏览器内核是多线程,至少包括三个常驻线程: JavaScript引擎线程 , GUI渲染线程, 浏览器事件触发线程 

JavaScript引擎是基于事件驱动单线程执行的,JavaScript引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JavaScript线程在运行JavaScript程序。

GUI渲染线程负责渲染浏览器界面,当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行。但需要注意,GUI渲染线程与JavaScript引擎是互斥的,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JavaScript引擎空闲时立即被执行。

事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JavaScript引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeout、也可来自浏览器内核的其他线程如鼠标点击、Ajax异步请求等,但由于JavaScript的单线程关系所有这些事件都得排队等待JavaScript引擎处理(当线程中没有执行任何同步代码的前提下才会执行异步代码)。

作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质, 所以很多人说这是伪多线程

setTimeout(function(){

  console.log(0);
},0)

console.log(1);

// 1

// 0

打印结果先打印1, 先执行了console.log(), 然后才执行 setTimeout()函数, 所以 setTimeout 是一个很有代表性的异步函数 当然还有 setInterval 以及典型的 ajax

大家都知道 js是单线程语言, 单线程就是代码一段一段执行, 按顺序执行,比如:

for(var i=0;i<100000;i++){

}

alert('hello world!!!');

  这段代码的意思是执行100...次后再执行alert,这样带来的问题是,严重堵塞了后面代码的执行,至于为什么,主要是因为JS是单线程的

那么什么是异步? js单线程是怎么实现异步的?

异步, 举个例子来说, 就好像你出去吃饭, 提前预定了, 但是由于人比较多, 没有位置了, 那么这个时候你可以出去逛个街啥的, 等人少的时候, 店家会通知你过去,你再过去吃饭就好了

比如 ajax 当 js单线程执行完 XMLHttpRequest.send() 之后,为了及时的得到响应内容, 在单线程中注册相应的事件就好, XMLHttpRequst.onReadyStateChange=fn(),  注册之后浏览器会在内部的其他线程监听事件, 直到事件被触发, 浏览器会在任务队列添加一个任务等待该单线程执行.

>>>任务队列

单线程就意味着所有的任务都要排队来执行,那么如果有耗时操作,那么后面的任务就很难得到执行, 为了解决这个问题, 所有就有了同步任务和异步任务, 同步任务是处在主线程上的任务,排队执行, 异步任务不进入主线程, 是在任务队列中的任务, 异步任务都必须要有回调函数,只有任务队列通知主线程, 某个异步任务可以执行了, 异步任务才会进入主线程被执行

执行顺序如下:

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。只有同步任务执行完才会去执行任务队列中的任务.

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步

一个js程序的单线程用来执行栈中的同步任务,当所有同步任务执行完毕后,栈被清空,然后读取任务队列中的一个待处理任务,并把相关回调函数压入栈中,单线程开始执行新的同步任务,执行完毕。

单线程从任务队列中读取任务是不断循环的,每次栈被清空后,都会在任务队列中读取新的任务,如果没有新的任务,就会等待,直到有新的任务,这就叫任务循环。因为每个任务都由一个事件所触发,所以也叫 EventLoop, setTimeout()函数就是一个异步任务, 被立刻添加到任务队列,等待执行, 等到同步任务执行完成之后, 才执行任务队列中的任务, 所以即使0秒 也是在 console.log()后面执行

>>>>回调函数事件

每个异步任务都要对应一个回调函数, 回调函数就是那些被主线程挂起的代码, 当异步任务开始执行的时候, 就是开始执行回调函数的时候

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

>>>> EventLoop

iOS 中有 runloop,也是一个事件循环机制, 可以让线程不断的循环运行, 取出队列中的任务处理, 没有任务的时候就让线程休眠,节省 CPU 资源, 还能提高效率, 和 eventloop有点相像, EventLoop是一个程序结构, 用于等待和发送消息事件, 这也是 JavaScript 可以单线程执行异步任务的机制, 简单说,就是在程序中设置两个线程:一个负责程序本身的运行,称为"主线程";另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为"Event Loop线程"(可以译为"消息线程")。

比如读取文件, 网络请求, 每当遇到I/O的时候,主线程就让Event Loop线程去通知相应的I/O程序,然后接着往后运行,所以不存在等待时间。等到I/O程序完成操作,Event Loop线程再把结果返回主线程, 这时就是函数回调的时候, 主线程就调用事先设定的回调函数,完成整个任务。

>>>>node.js 的运行机制

node.js 支持多线程

(1)V8引擎解析JavaScript脚本。

(2)解析后的代码,调用Node API。

(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。

(4)V8引擎再将结果返回给用户。

node

process.nextTick方法可以在当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像

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

由于process.nextTick方法指定的回调函数,总是在当前"执行栈"的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行

setImmediate(function A() {
  console.log(1);
  setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0);

setImmediate与setTimeout(fn,0)各自添加了一个回调函数A和timeout,都是在下一次Event Loop触发。那么,哪个回调函数先执行呢?答案是不确定。运行结果可能是1--TIMEOUT FIRED--2,也可能是TIMEOUT FIRED--1--2。

令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。process.nextTick和setImmediate的一个重要区别:多个process.nextTick语句总是在当前"执行栈"一次执行完,多个setImmediate可能则需要多次loop才能执行完。事实上,这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取"事件队列"!

1
2
3
4
5
6
7
8
setTimeout(function() {
console.log("a")
}, 0)
for(let i=0; i<10000; i++) {}
console.log("b")
// 结果:b a

打印结果表明回调函数并没有立刻执行,而是等待栈中的任务执行完毕后才执行的。栈中的任务执行多久,它就得等多久。

JavaScript单线程和其异步机制就如上所述。所谓的单线程并不孤单,它的背后有浏览器的其他线程为其服务,其异步也得靠其他线程来监听事件的响应,并将回调函数推入到任务队列等待执行。单线程所做的就是执行栈中的同步任务,执行完毕后,再从任务队列中取出一个事件(没有事件的话,就等待事件),然后开始执行栈中相关的同步任务,不断的这样循环

>>>几种 js异步函数实现方式

callback 方式 也是最早的最原始的, 清晰明了, 缺点就是多层嵌套的时候代码看起来就很糟糕了, 回调地狱

 promise 方式 比较常用的 .then()可以链式调用 但是还是不够清晰明了

 async await 这是 es7的异步解决方案 , 主要还是 await 一个 promise, 只是调用起来更加清晰直观, 看起来就像同步函数一样

 像上面这个 await 异步函数 , 打印结果为 2秒后打印 world, 再隔1 秒后打印 hello , 最后打印 helloword 

原文地址:https://www.cnblogs.com/ChrisZhou666/p/8520025.html