我对异步操作的理解正确吗?

时间:2020-07-18 15:56:47

标签: javascript

我正在努力掌握异步性的概念。关于异步操作,以下内容大致正确吗?

  1. 如果一段代码需要很长时间才能完成,则可能会出现问题。这是因为i)它阻止了下面的代码的运行,并且在将硬代码加载到后台的同时运行它可能会更好。 ii)实际上,JS可能会在硬代码完成之前尝试执行以下代码。如果下面的代码依赖于硬代码,那就有问题。

  2. 一种解决方案是:如果某个操作需要很长时间才能完成,则您希望在处理原始线程时在单独的线程中对其进行处理。只要确保主线程没有引用异步操作返回的内容即可。 JS为该解决方案采用了事件队列。异步操作在事件队列中执行,该事件队列在主线程之后执行。

  3. 但是即使事件队列也可能遭受与主线程相同的问题。如果位于fetch2上方的fetch1需要很长时间才能返回承诺,而fetch2却没有,则JS可能会在执行fetch1之前开始执行fetch2。这是Promise.all有用的地方,因为在fetch1和fetch2的承诺都得到解决之前,它不会继续进行异步操作的下一步。

  4. 在单独的注释中,我在链接.then时已读过,这被视为一个异步操作,因此我们始终保证后续的.then仅在{{1 }}在履行|兑现承诺之前。

2 个答案:

答案 0 :(得分:3)

几乎正确,但不太正确。

如果您正在谈论英语中的“异步性”,那么这意味着事情可能会发生混乱。此概念已在许多语言中使用,包括Java和C / C ++中的多线程。

如果您要谈论异步性的特定概念,因为它与C / C ++中的node.js或异步I / O有关,那么您对它在底层的工作方式确实有一些误解。

如果一段代码需要很长时间才能完成,则可能会出现问题。这是因为i)它阻止了下面的代码的运行,并且在将硬代码加载到后台的同时运行它可能会更好。 ii)实际上,JS可能会在硬代码完成之前尝试执行以下代码。如果下面的代码依赖于硬代码,那就有问题。

当谈论C / C ++中的javascript或异步I / O(javascript从中获得异步性)时,这是不正确的。

实际发生的事情是等待某件事发生可能需要很长时间才能完成。与其等着,为什么不告诉操作系统一旦事情发生就执行一些代码(您的回调)。

在OS级别,大多数现代操作系统都具有API,可让您告诉它在发生某些情况时唤醒您的进程。那可能是键盘事件,鼠标事件,I / O事件(来自磁盘或网络),系统重新配置事件(例如,更改显示器分辨率)等。

大多数传统语言实现阻塞I / O。发生的情况是,当您尝试从磁盘或网络中读取内容时,进程将立即进入睡眠状态,并且操作系统将在数据到达时再次将其唤醒:

Traditional blocking I/O

 time
  │
  ├────── your code doing stuff ..
  ├────── read_data_from_disk() ───────────────────┐
  ┆                                                ▼
  :                                        OS puts process to sleep
  .
  .                                         other programs running ..
  .
  :                                            data arrives ..
  ┆                                        OS wakes up your process   
  ├────── read_data_from_disk() ◀──────────────────┘
  ├────── your program resume doing stuff ..
  ▼

这意味着您的程序一次只能等待一件事。这意味着您的程序大多数时候都没有使用CPU。侦听更多事件的传统解决方案是多线程。每个线程都将分别阻塞其事件,但是您的程序可以为它感兴趣的每个事件产生一个新线程。

事实证明,每个线程等待一个事件的幼稚多线程处理速度很慢。而且最终会消耗大量RAM,尤其是对于脚本语言。因此,这不是javascript所做的。

注意:从历史上看,javascript使用单线程而不是多线程是一个偶然的事实。这只是团队做出的决定的结果,该决定将渐进式JPEG渲染和GIF动画添加到了早期的浏览器中。碰巧的是,这正是使node.js之类的东西快速运行的原因。

javascript要做的是等待多个事件,而不是等待单个事件。所有现代操作系统都有API,可让您等待多个事件。它们的范围从BSD和Mac OSX上的queue / kqueue到Linux上的poll/epoll到Windows上的重叠I / O到跨平台POSIX select()系统调用。 >

javascript处理外部事件的方式类似于以下内容:

Non-blocking I/O (also known as asynchronous I/O)

 time
  │
  ├────── your code doing stuff ..
  ├────── read_data_from_disk(read_callback) ───▶ javascript stores
  │                                               your callback and
  ├────── your code doing other stuff ..          remember your request
  │
  ├────── wait_for_mouse_click(click_callback) ─▶ javascript stores
  │                                               your callback and
  ├────── your code doing other stuff ..          remember your request
  │
  ├────── your finish doing stuff.
  ┆       end of script ─────────────▶ javascript now is free to process
  ┆                                  pending requests (this is called
  ┆                                  "entering the event loop").
  ┆                                  Javascript tells the OS about all the
  :                                  events it is interested in and waits..
  .                                            │
  .                                            └───┐
  .                                                ▼
  .                                        OS puts process to sleep
  .
  .                                         other programs running ..
  .
  .                                            data arrives ..
  .                                        OS wakes up your process
  .                                                │
  .                                            ┌───┘
  :                                            ▼
  ┆                       Javascript checks which callback it needs to call
  ┆                          to handle the event. It calls your callback.
  ├────── read_callback() ◀────────────────────┘
  ├────── your program resume executing read_callback
  ▼

主要区别在于同步多线程代码在每个线程中等待一个事件。诸如javascript之类的单线程或诸如Nginx或Apache之类的多线程异步代码会在每个线程中等待多个事件。

注意:Node.js在单独的线程中处理磁盘I / O,但所有网络I / O均在主线程中处理。这主要是因为异步磁盘I / O API在Windows和Linux / Unix之间不兼容。但是,可以在主线程中执行磁盘I / O。 Tcl语言是在主线程中执行异步磁盘I / O的一个示例。

一种解决方案是:如果一项操作需要很长时间才能完成,则您希望在处理原始线程的同时在单独的线程中进行处理。

除了Web worker(或Node.js中的worker线程)之外,javascript中的异步操作不会发生这种情况。如果是网络工作者,则可以,您正在其他线程中执行代码。

但是,即使事件队列也可能遭受与主线程相同的问题。如果位于fetch2上方的fetch1需要很长的时间才能返回承诺,而fetch2却没有,则JS可能会在执行fetch1之前开始执行fetch2

这不是正在发生的事情。您正在执行的操作如下:

fetch(url_1).then(fetch1); // tell js to call fetch1 when this completes
fetch(url_2).then(fetch2); // tell js to call fetch2 when this completes

并不是js“可能”开始执行。上面的代码发生的事情是两个获取都是同步执行的。也就是说,第一次抓取严格发生在第二次抓取之前。

但是,以上所有代码都告诉javascript在以后的某个时间回调函数fetch1fetch2这是要记住的重要课程上面的代码不会执行 fetch1fetch2函数(回调)。您要做的就是告诉javascript在数据到达时调用它们。

如果您执行以下操作:

fetch(url_1).then(fetch1); // tell js to call fetch1 when this completes
fetch(url_2).then(fetch2); // tell js to call fetch2 when this completes

while (1) {
    console.log('wait');
}

然后fetch1fetch2 将永远不会执行

我会在这里暂停,让您考虑一下。

记住异步I / O的处理方式。实际上,所有I / O(通常称为异步)函数调用均不会导致I / O立即被访问。他们所做的只是提醒javascript您想要某些东西(单击鼠标,网络请求,超时等),并且您希望javascript在该东西完成时稍后执行功能。 异步I / O仅在没有更多代码可执行时才在脚本末尾处理

这确实意味着您不能在JavaScript程序中使用无限while循环。不是因为javascript不支持它,而是有一个内置的while循环围绕着您的整个程序:这个大的while循环称为事件循环。

另外,我在链接时已读过。那么,这算是一个异步操作。

是的,这是设计使然,以避免在处理承诺时使人们感到困惑。

如果您对操作系统如何处理所有这些而不需要进一步创建线程感兴趣,那么您可能会对我对以下相关问题的回答感兴趣:

Is there any other way to implement a "listening" function without an infinite while loop?

node js - what happens to incoming events during callback excution

TLDR

如果没有别的我想让您了解两件事:

  1. Javascript是严格的同步编程语言。代码中的每个语句都严格按顺序执行。

  2. 所有语言(是的,包括C / C ++以及Java和Python等)的异步代码都将在以后的任何时间调用您的回调。您的回调不会立即被调用。异步性是一个函数调用级别的概念。

就异步性而言,并不是说javascript有什么特别的*。只是大多数javascript库默认情况下都是异步的(尽管您也可以用任何其他语言编写异步代码,但它们的库通常默认情况下是同步的)。

*注意:当然,诸如async / await之类的确使javascript更具有处理异步代码的能力。

侧面说明:承诺没有什么特别的。这只是一种设计模式。它不是javascript语法内置的东西。只是Promises随附了较新版本的javascript,并将其作为其标准库的一部分。即使在非常老版本的javascript和其他语言中,您也可能一直使用诺言(例如,在Java8及更高版本中,调用在其标准库中具有诺言,但将它们称为Future)。

答案 1 :(得分:2)

此页面很好地解释了Node.js事件循环(因为它是由Node.js产生的):

https://nodejs.dev/learn/the-nodejs-event-loop

如果一段代码需要很长时间才能完成,则可能会出现问题。 这是因为我)它阻止了下面的代码运行,并且可能是 很高兴在硬代码在后台加载的同时运行它。和ii) 确实,JS可能会在硬编码之前尝试执行以下代码 完成。如果下面的代码依赖于硬代码,那就有问题。

是的,尽管要改写:让用户等待代码解析是糟糕的用户体验,而不是运行时错误。但是,如果代码块依赖尚未返回的未定义返回的变量,则确实会发生运行时错误。

一个解决方案是:如果一项操作需要很长时间才能完成,则需要 在原始线程为 处理。只要确保主线程没有引用任何东西 异步操作返回的值。 JS使用事件队列 这个解决方案。异步操作在事件队列中执行 在主线程之后执行。

是的,但是重要的是要指出这些其他线程正在浏览器或api服务器中发生,而不是在JS脚本中发生。另外,异步函数仍在主调用堆栈中被调用,但是它们的解析被放在作业队列或消息队列中。

但是,即使事件队列也可能遭受与 主线程。如果位于fetch2上方的fetch1需要很长时间 是时候返回承诺,而fetch2却没有,JS可能会开始执行 在执行fetch1之前先执行fetch2。这是Promise.all有用的地方 因为它将不会继续进行异步中的下一步 直到fetch1和fetch2的承诺都得到解决为止。

是的,一旦调用堆栈中的当前函数解析,就将执行promise的解析。如果一个承诺先于另一个承诺解决,则其解决将在第一个事件(如果称为第二个)中执行。还请注意,Promise.all不会更改解决时间,而是会按照执行诺言的顺序一起返回解决。

在单独的注释中,我在链接时已阅读过。然后,这算作一个 异步操作,因此我们始终可以保证后续 .then仅在执行之前的.then执行时执行 它的诺言。

是的,尽管更新和更干净的语法是异步等待的:

function A () {
  return new Promise((resolve, reject)=> setTimeout(()=> resolve("done"),2000))
}

async function B () {
  try {
    console.log("waiting...");
    const result = await A();
    console.log(result);
  } catch (e) {
    console.log(e);
  }
}

B();

下面显示了运行中的Node.js调用堆栈:

function A () {
  return new Promise((resolve, reject)=> resolve(console.log("A")))
}

function B () {
  console.log("B");
  C();
}

function C () {
  console.log("C"); 
}

function D () {
  setTimeout(()=> console.log("D"),0);
}


B();
D();
A();
 

B被调用,C被调用,B解析,D被调用,setTimeout被调用,但是其解析被移到消息队列,D解析,A被调用,promise被调用并立即解析,A解析,调用堆栈完成,访问了消息队列