【问题标题】:Single thread synchronous and asynchronous confusion单线程同步和异步的混淆
【发布时间】:2017-09-14 22:02:40
【问题描述】:

假设makeBurger() 需要 10 秒

在同步程序中,

function serveBurger() {
   makeBurger();
   makeBurger();
   console.log("READY") // Assume takes 5 seconds to log. 
}

这总共需要 25 秒才能执行。

所以对于 NodeJs,假设我们制作了 makeBurgerAsync() 的异步版本,这也需要 10 秒。

function serveBurger() {
   makeBurgerAsync(function(count) {

   });
   makeBurgerAsync(function(count) {

   });  
   console.log("READY") // Assume takes 5 seconds to log. 
}

因为它是单线程。我很难想象幕后到底发生了什么。

  1. 所以当函数运行时,两个异步函数都会进入事件循环,console.log("READY") 会立即执行。
  2. 但是当console.log("READY") 正在执行时,两个异步函数都没有真正完成工作,对吧?由于单线程占用了 console.log 5 秒。
  3. console.log 完成后。 CPU 将有时间在两个异步之间切换,以便每次可以运行每个函数的一点点。

所以根据这个,函数不一定会导致更快的执行,async 可能由于在事件循环之间切换而变慢?我想,在一天结束的时候,一切都将分散在一个线程上,这与同步版本是一样的吗?

我可能错过了一些非常重要的概念,所以请告诉我。谢谢。

编辑 如果异步操作类似于查询数据库等,这是有道理的。基本上,nodejs 只会说“嘿,数据库为我处理这个,而我会做其他事情”。但是,我不理解的情况是nodejs本身内部的自定义回调函数。

EDIT2

function makeBurger() {
    var count = 0;
    count++; // 1 time
    ...
    count++; // 999999 times
    return count;
}

function makeBurgerAsync(callback) {
    var count = 0;
    count++; // 1 time
    ...
    count++; // 999999 times
    callback(count);
}

【问题讨论】:

  • 这取决于 makeBurgerAsync 是更像是在事件循环中推迟执行的 setTimeout 还是更像是在实际方法完成后才执行连续执行的等待。在后一种情况下,console.log 不会在 5 秒内完成,它会在两个调用完成后出现。
  • 这取决于为什么完成“异步”操作需要 10 秒。如果这 10 秒用于阻塞事件循环,那么是的,它将阻塞 10 秒,但是它只会阻塞 console.log 发生之后,因为需要 10 秒的操作本来是推到回调队列。 (并且它会阻止在这 10 秒窗口期间进入的任何请求)如果这 10 秒用于等待,例如,文件 i/o 或 http,因此没有阻塞事件循环,则没有明显的阻塞。
  • @WiktorZychla ""更像是等待在实际方法完成后才执行持续的东西" 你能澄清一下这个说法吗?谢谢你的回复!
  • 澄清:js中没有fooAsync这样的东西。要么是fooAsync(callback),它在完成后立即返回并执行回调,要么是await fooAsync,它在完成后执行下一行。您的示例 makeBurgerAsync 需要明确说明您实际指的是哪种异步执行。
  • 我的意思是......它没有让你知道什么时候它是异步的@WiktorZychla。

标签: node.js asynchronous


【解决方案1】:

在 node.js 中,所有异步操作都在 node.js Javascript 单线程之外完成它们的任务。它们要么使用本机代码线程(例如 node.js 中的磁盘 I/O),要么根本不使用线程(例如事件驱动的网络或计时器)。

您不能采用完全用 node.js Javascript 编写的同步操作并神奇地使其异步。异步操作是异步的,因为它调用了一些以本机代码实现并以实际异步方式编写的函数。因此,要使某些东西异步,必须专门编写它以使用本身与异步本机代码实现异步的较低级别的操作。

这些带外操作,然后通过事件队列与主 node.js Javascript 线程通信。当其中一个异步操作完成时,它会向 Javascript 事件队列添加一个事件,然后当单个 node.js 线程完成它当前正在执行的操作时,它会从事件队列中获取下一个事件并调用与该事件关联的回调.

因此,您可以并行运行多个异步操作。并且并行运行 3 个操作通常比按顺序运行相同的 3 个操作具有更短的端到端运行时间。

让我们检查一个真实的异步情况,而不是你的伪代码:

function doSomething() {
   fs.readFile(fname, function(err, data) {
       console.log("file read");
   });
   setTimeout(function() {
       console.log("timer fired");
   }, 100);

   http.get(someUrl, function(err, response, body) {
       console.log("http get finished");
   });

   console.log("READY");
}

doSomething();

console.log("AFTER");

这是一步一步发生的事情:

  1. fs.readFile() 已启动。由于 node.js 使用线程池实现文件 I/O,因此该操作被传递给 node.js 中的一个线程,它将在一个单独的线程中运行。
  2. 无需等待fs.readFile() 完成,就会调用setTimeout()。这使用了 libuv(构建 node.js 的跨平台库)中的计时器子系统。这也是非阻塞的,因此会注册计时器,然后继续执行。
  3. http.get() 被调用。这将发送所需的 http 请求,然后立即返回进一步执行。
  4. console.log("READY") 将运行。
  5. 三个异步操作将以不确定的顺序完成(先完成其操作的将先完成)。出于讨论的目的,假设setTimeout() 首先完成。当它完成时,node.js 中的一些内部将在事件队列中插入一个事件,其中包含计时器事件和注册的回调。当 node.js 主 JS 线程执行完任何其他 JS 后,它会从事件队列中抓取下一个事件并调用与之关联的回调。
  6. 出于本描述的目的,假设在执行计时器回调时,fs.readFile() 操作完成。使用它自己的线程,它会在 node.js 事件队列中插入一个事件。
  7. 现在setTimeout() 回调结束。此时,JS 解释器会检查事件队列中是否还有其他事件。 fs.readfile() 事件在队列中,因此它会抓取该事件并调用与之关联的回调。该回调执行并完成。
  8. 一段时间后,http.get() 操作完成。在 node.js 内部,一个事件被添加到事件队列中。由于事件队列中没有其他内容并且 JS 解释器当前未执行,因此可以立即处理该事件并且可以调用 http.get() 的回调。

根据上述事件序列,您会在控制台中看到:

READY
AFTER
timer fired
file read
http get finished

请记住,这里最后三行的顺序是不确定的(它只是基于不可预测的执行速度),因此这里的精确顺序只是一个示例。如果您需要按特定顺序执行这些操作,或者需要知道这三个操作何时完成,那么您必须添加额外的代码来跟踪它。


由于您似乎试图通过使当前不是异步的异步操作来使代码运行得更快,所以让我重复一遍。您不能采用完全用 Javascript 编写的同步操作并“使其异步”。您必须从头开始重写它以使用根本不同的异步较低级别操作,或者您必须将其传递给其他进程执行,然后在完成时收到通知(使用工作进程或外部进程或本机代码插件或类似的东西)。

【讨论】:

  • 这解决了我的困惑这么多!谢谢你这么详细的帖子。您不能采用完全用 Javascript 编写的同步操作并“使其异步”。但是,我可以在同步函数上设置超时,让它有机会进入事件循环。但它没有什么好处。我猜 asynchornous 与其他不会占用线程的资源进行通信的意义。
  • 你提到“fs.readFile()被启动了。由于node.js使用线程池实现文件I/O,所以这个操作被传递给node.js中的一个线程,它会在那里运行在一个单独的线程中。”这让我觉得 node.js 间接使用了多个线程?
  • @Zanko - 当人们在 node.js 的内容中说“单线程”时,他们指的是 JS 解释器。你的 JS 代码只运行在单线程中,永远不可能两段 Javascript 之间的线程冲突。有一些使用本机代码的库函数(其中fs.readFile() 是一个例子)确实使用线程来实现异步功能。这仍然不会以任何方式使 JS 解释器成为多线程的。异步函数通过事件队列与JS解释器的单线程进行通信和同步。
猜你喜欢
  • 2011-01-06
  • 1970-01-01
  • 2021-03-17
  • 2020-11-02
  • 1970-01-01
  • 2019-01-23
  • 1970-01-01
  • 2014-07-14
  • 1970-01-01
相关资源
最近更新 更多