General asynchronous programming concepts

 

在本文中,我们将介绍与异步编程有关的许多重要概念,以及在Web浏览器和JavaScript中的外观. 在阅读本模块中的其他文章之前,您应该了解这些概念.

Prerequisites: Basic computer literacy, a reasonable understanding of JavaScript fundamentals.
Objective: 了解异步编程背后的基本概念,以及它们如何在Web浏览器和JavaScript中体现.

Asynchronous?

通常,给定程序的代码直接运行,一次只发生一件事. 如果一个函数依赖于另一个函数的结果,则它必须等待另一个函数完成并返回,直到这种情况发生为止,从用户的角度来看,整个程序实际上都已停止.

例如,Mac用户有时会遇到旋转的彩虹色光标(通常称为"沙滩球"). 光标就是操作系统说的:"您正在使用的当前程序不得不停止并等待某些操作完成,而且时间太长了,我担心您会想知道发生了什么."

Multi-colored macOS beachball busy spinner

这是一个令人沮丧的经历,并且不能很好地利用计算机的处理能力,尤其是在计算机具有多个可用处理器核心的时代. 当您可以让其他任务在另一个处理器内核上运行并让您知道何时完成时,坐在那里等待某事毫无意义. 这使您可以同时完成其他工作,这是异步编程的基础. 取决于您使用的编程环境(在Web开发的情况下是Web浏览器)为您提供API,这些API允许您异步运行此类任务.

Blocking code

异步技术非常有用,尤其是在Web编程中. 当Web应用程序在浏览器中运行并且执行大量代码而不将控制权交还给浏览器时,浏览器可能似乎被冻结. 这称为阻塞 ; 浏览器被阻止继续处理用户输入和执行其他任务,直到Web应用返回处理器的控制权为止.

让我们看几个示例,这些示例显示了阻塞的含义.

在我们的simple-sync.html示例中( 看到它实时运行 ),我们将click事件监听器添加到按钮上,以便在单击按钮时运行一个耗时的操作(计算1000万个日期,然后将最后一个记录到控制台)然后将一段添加到DOM:

const btn = document.querySelector('button');
btn.addEventListener('click', () => {
  let myDate;
  for(let i = 0; i < 10000000; i++) {
    let date = new Date();
    myDate = date
  }

  console.log(myDate);

  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

在运行示例时,打开JavaScript控制台,然后单击按钮-您会注意到,只有在日期计算完成并记录了控制台消息之后,该段落才会出现. 代码按照其在源代码中出现的顺序运行,并且直到较早的操作完成运行之后,才会运行较后的操作.

注意 :前面的示例非常不现实. 您永远不会在一个真正的Web应用程序上计算出1000万个日期! 但是,它确实可以为您提供基本概念.

在我们的第二个示例simple-sync-ui-blocking.html (在线观看 )中,我们模拟了一些实际的东西,您可能会在实际页面上遇到这些东西. 我们通过呈现UI来阻止用户交互. 在此示例中,我们有两个按钮:

  • 单击"填充画布"按钮,将可用的<canvas>填充一百万个蓝色圆圈.
  • 一个"单击我以发出警报"按钮,单击后将显示一条警报消息.
function expensiveOperation() {
  for(let i = 0; i < 1000000; i++) {
    ctx.fillStyle = 'rgba(0,0,255, 0.2)';
    ctx.beginPath();
    ctx.arc(random(0, canvas.width), random(0, canvas.height), 10, degToRad(0), degToRad(360), false);
    ctx.fill()
  }
}

fillBtn.addEventListener('click', expensiveOperation);

alertBtn.addEventListener('click', () =>
  alert('You clicked me!')
);

如果您单击第一个按钮,然后快速单击第二个按钮,您将看到直到渲染完圆圈后才会显示警报. 第一个操作将阻止第二个操作,直到完成运行为止.

注意 :好的,在我们的情况下,这很丑陋,并且我们伪造了屏蔽效果,但这是一个真正的应用程序开发人员一直在努力缓解的常见问题.

为什么是这样? 答案是因为JavaScript通常是单线程的 . 在这一点上,我们需要介绍线程的概念.

Threads

线程基本上是程序可以用来完成任务的单个进程. 每个线程一次只能执行一个任务:

Task A --> Task B --> Task C

每个任务将按顺序运行; 必须先完成一项任务,然后才能开始下一项任务.

如前所述,许多计算机现在具有多个内核,因此可以一次执行多个操作. 可以支持多个线程的编程语言可以使用多个内核来同时完成多个任务:

Thread 1: Task A --> Task B
Thread 2: Task C --> Task D

JavaScript is single threaded

传统上,JavaScript是单线程的. 即使有多个内核,也只能使它在称为主线程的单个线程上运行任务. 我们上面的示例是这样运行的:

Main thread: Render circles to canvas --> Display alert()

一段时间后,JavaScript获得了一些工具来解决此类问题. Web worker允许您将一些JavaScript处理发送到一个单独的线程(称为worker),以便您可以同时运行多个JavaScript块. 通常,您会使用一个工作程序在主线程上运行昂贵的进程,从而不会阻止用户交互.

  Main thread: Task A --> Task C
Worker thread: Expensive task B

考虑到这一点,再次打开浏览器的JavaScript控制台,看看simple-sync-worker.html看到它实时运行 ). 这是对先前示例的重写,该示例在单独的工作线程中计算1000万个日期. 现在,当您单击按钮时,浏览器将能够在日期完成计算之前显示该段落. 第一个操作不再阻塞第二个.

Asynchronous code

Web worker非常有用,但是确实有其局限性. 一个主要的问题是他们无法访问DOM-您无法让工作人员直接做任何事情来更新UI. 我们无法在工人内部渲染100万个蓝色圆圈. 它基本上可以做数字运算.

第二个问题是,尽管在工作程序中运行的代码没有阻塞,但基本上仍是同步的. 当一个函数依赖于多个先前处理的结果来起作用时,这将成为一个问题. 请考虑以下线程图:

Main thread: Task A --> Task B

在这种情况下,假设任务A正在执行从服务器获取图像的操作,然后任务B会对图像执行了操作,例如对其应用了过滤器. 如果您启动正在运行的任务A,然后立即尝试运行任务B,则会出现错误,因为该图像尚不可用.

  Main thread: Task A --> Task B --> |Task D|
Worker thread: Task C -----------> |      |

在这种情况下,假设任务D同时使用了任务B和任务C的结果.如果我们可以保证这些结果可以同时使用,那么我们可能还可以,但是这不太可能. 如果任务D的输入之一尚不可用时尝试运行,它将抛出错误.

为了解决这些问题,浏览器允许我们异步运行某些操作. 诸如Promises之类的功能使您可以设置一个正在运行的操作(例如,从服务器中获取图像),然后等到结果返回后再运行另一个操作:

Main thread: Task A                   Task B
    Promise:      |__async operation__|

由于操作发生在其他地方,因此在处理异步操作时不会阻塞主线程.

在下一篇文章中,我们将开始研究如何编写异步代码. 令人兴奋的东西,对吧? 继续阅读!

Conclusion

现代软件设计越来越多地围绕使用异步编程,以允许程序一次执行多个操作. 当您使用更新,更强大的API时,会发现更多的情况是唯一的异步方法. 过去很难编写异步代码. 它仍然需要习惯,但是变得容易得多. 在本模块的其余部分中,我们将进一步探讨异步代码为何如此重要以及如何设计避免上述某些问题的代码.

In this module