Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[译]如何理解并发模型(concurrency model)和事件循环(event loop)? #216

Open
FrankKai opened this issue May 5, 2020 · 1 comment

Comments

@FrankKai
Copy link
Owner

FrankKai commented May 5, 2020

  • 初识并发模型和事件循环
  • 运行时概念
    • 可视化呈现
    • 栈(stack)
    • 堆(Heap)
    • 队列(Queue)
  • 事件循环
    • 初识事件循环
    • ”执行到完成“
    • 增加消息
    • 零延迟(Zero delay)
    • 多个运行时互相通信
  • 从不阻塞
@FrankKai
Copy link
Owner Author

FrankKai commented May 5, 2020

初识并发模型和事件循环

JavaScript拥有基于event loop的并发模型。
它和C和Java的语言的model非常不同。

基于event loop的并发模型是什么?

  • 负责代码的执行
  • 收集和处理事件
  • 执行队列子任务

运行时概念

下面的这些部分解释了理论模型。
现代的JavaScript引擎实现并且极大程度的去优化了这些实现。

可视化呈现

image

栈(stack)

从frames stack中调用函数。

function foo(){
    let a = 10;
    return a + b + 11
}
function bar(x){
    let y = 3;
    return foo(x*y);
}
console.log(bar(7)); // 返回42

过程拆解:

  • 调用bar时,包含bar的arguments和本地变量的第一个frame创建了
  • 当bar调用foo时,包含了foo的arguments和本地变量的第二个frame创建成功,并且将其推入顶部
  • 当foo返回时,顶部的frame element会从stack中pop出来(留下bar在栈中)
  • 当bar返回时,stack清空

可视化拆解:

image

image

image

堆(Heap)

对象被分配在heap中,之所以称之为堆是因为它会占用大量的内存(很多都是非结构化的)。

队列(Queue)

JavaScript运行时使用的是message queue,它的意思是有一个消息列表去处理。
为了处理消息,每个消息都有一个与之相关的函数去调用。

在事件循环的一些点,runtime开始处理队列中的消息,从最老的一个开始。为了做到这样,消息会从队列中移除,并且使用消息作为输入参数调用相应的函数。通常来讲,调用一个函数会在栈中生成一个新的frame。

函数的处理一直会进行,知道stack被清空。然后event loop去处理下一个在队列中的消息。

如何理解”使用消息作为输入参数调用相应的函数“?

上面的例子中,foo执行完之后会从call stack中出栈,也就是说这个foo的消息从消息队列中移除。
来思考一下,仅仅是移除了就完事了吗?
当然不是。
foo执行完毕之后,会将返回值作为输入值传到bar中,调用return foo(x*y)
使用foo这个消息作为输入参数去调用bar这个函数。
也就是我们说的”使用消息作为输入参数调用相应的函数。“

事件循环

初识事件循环

event loop之所以叫这个名字,是因为它的实现方式,可以看成下面的伪代码:

while(queue.waitForMessage()){
    queue.processNextMessage()
}

queue.waitForMessage()同步等待消息到达(如果有一个消息是可用并且是等待处理的。)

”执行到完成“

每个消息都会在下一个消息处理完成前完成。

这为你的程序带来了很大的好处,其中包括:无论何时调用一个函数,它都不能被预清空而且会在任何的代码运行前完全运行(而且可以修改函数操作的数据)。这和C不一样,例如:如果一个函数运行在一个线程中,它也许会在任何时间被运行系统停止,然后去其它的线程去运行其他的代码。

这种模型有一个缺点:如果一个消息花了很长时间才能完成,web应用不能到达去处理用户的click或者scroll事件。浏览器可以通过一个”script占用过长的时间去执行“的对话框告知用户。一个很好的实践是使得message变短,并且尽可能将一个消息拆解成多个消息。

增加消息

在web浏览器中,消息可以在时间发生的任意时间被添加,并且会有一个事件listener附加在其上。如果没有listener,event会丢失。所以当一个click事件发生在元素上时,click事件的处理器会添加一个message。其他任何的事件都是这样的。

setTimeout添加message到queue的过程
  • setTimeout与message queue的关系
  • 为什么有些时候setTimeout的延迟时间会不准?

setTimeout有两个参数:第一callback是被增加到queue的message,第二个是最小时间(默认为0)。time的值代表着message被推入到queue的最小时间。如果没有其他的消息在queue中,并且这个栈是空的,消息会在delay的时间后被处理。然而,如果有消息,setTimeout的消息需要等待其他的消息处理完成后再执行。 因为这个原因,第二个参数代表着最小时间,并不是保证时间。

“setTimeout不能准时执行”可以看下面这个例子去理解:

const s = new Date().getSeconds();

setTimeout(function() {
  // 打印出了2,意味着并没有准时在500ms后执行
  console.log("在(new Date().getSeconds() - s)秒后打印。");
}, 500)

while (true) {
  if (new Date().getSeconds() - s >= 2) {
    console.log("循环了2秒")
    break;
  }
}

零延迟(Zero delay)

零延迟的意思是函数不能在0ms后立即执行。
比如setTimeout传入了一个0毫秒的时间延迟,但是它并不会在0毫秒后执行回调函数。

执行需要依赖队列中等待的任务数量。在下面的例子中,消息this is just a message会在回调中的消息被处理之前被写入console,因为delay是最小的时间并不是精准的保证时间。

基本上,setTimeout需要等待队列消息中的所有代码执行完成,即使你为setTimout指定了一个精准的时间。

(function() {

  console.log('this is the start');

  setTimeout(function cb() {
    console.log('Callback 1: this is a msg from call back');
  }); // has a default time value of 0

  console.log('this is just a message');

  setTimeout(function cb1() {
    console.log('Callback 2: this is a msg from call back');
  }, 0);

  console.log('this is the end');

})();

// "this is the start"
// "this is just a message"
// "this is the end"
// "Callback 1: this is a msg from call back"
// "Callback 2: this is a msg from call back"

多个运行时互相通信

一个web worker或者是跨域的iframe都有自己的stack,heap和消息队列。
两个截然不同的runtime(运行时)可以通过postMessage方法去互相传递消息。
这个方法会把消息增加到另一个运行时中(如果后者有一个message事件的话)。

从不阻塞

JavaScript的事件循环模型有一个非常有趣的属性,它不像其他的语言,js不会阻塞。
通过events和callbacks处理I/O,所以当应用等待IndexedDB的查询结果时,或者是等待XHR请求返回时,它能够处理其他类似用户输入这些事情。
有一些遗留的异常类似alert或者同步XHR,但是最好是避免使用它们。
异常的异常是存在的,用遗留的异常只能带来bug,带不来别的东西。

参考资料:https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop

@FrankKai FrankKai changed the title 如何理解并发模型(concurrency model)和事件循环(event loop)? [译]如何理解并发模型(concurrency model)和事件循环(event loop)? May 6, 2020
@FrankKai FrankKai mentioned this issue Aug 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant