Javascript 异步函数和 Web worker 之间的区别?

Difference between Javascript async functions and Web workers?

在线程方面,web worker 和声明为

的函数有什么区别
async function xxx()
{
}

?

我知道网络工作者是在不同的线程上执行的,但是异步函数呢?这些函数的线程化方式是否与通过 setInterval 执行的函数相同,或者它们是否受制于另一种不同类型的线程化?

WebWorkers 相比,async 函数永远无法保证在单独的线程上执行。

他们只是在响应到达之前不会阻塞整个线程。您可以将它们视为被注册为 waiting 结果,让其他代码执行,当它们的响应通过时它们被执行;因此得名 异步 编程。

这是通过消息队列实现的,消息队列是要处理的消息列表。每条消息都有一个关联的函数,该函数被调用以处理消息。

这样做:

setTimeout(() => {
  console.log('foo')
}, 1000)

将简单地将回调函数(记录到控制台)添加到消息队列。当它的 1000ms 计时器过去时,消息从消息队列中弹出并执行。

当计时器计时时,其他代码可以自由执行。这就是多线程的 错觉

上面的 setTimeout 示例使用了回调。 Promisesasync 在较低级别上的工作方式相同 — 它们依托于 message-queue 概念,但只是语法上不同。

async 函数只是语法糖 Promises 和它们是回调的包装器。

// v await is just syntactic sugar
//                 v Promises are just wrappers
//                                         v functions taking callbacks are actually the source for the asynchronous behavior
   await new Promise(resolve => setTimeout(resolve));
 

现在可以通过代码立即回调回调,例如如果你 .filter 一个数组,或者引擎可以在某个地方内部存储回调。然后,当特定的 事件 发生时,它会执行回调。可以说这些是 异步回调 ,而那些通常是我们包装到 Promises 中的回调 await 它们。

为了确保两个回调不会同时运行(这会使并发修改成为可能,这会造成很多麻烦)每当事件发生时,事件不会立即得到处理,而是a Job (callback with arguments) gets placed into a Job Queue. Whenever the JavaScript Agent (= thread²) 完成当前作业的执行,它会在该队列中查找下一个要处理的作业¹。

因此可以说 async function 只是一种表达 连续工作系列的方式 .

 async function getPage() {
   // the first job starts fetching the webpage
   const response = await fetch("https://whosebug.com"); // callback gets registered under the hood somewhere, somewhen an event gets triggered
   // the second job starts parsing the content
   const result = await response.json(); // again, callback and event under the hood
   // the third job logs the result
   console.log(result);
}

// the same series of jobs can also be found here:
fetch("https://whosebug.com") // first job
   .then(response => response.json()) // second job / callback
   .then(result => console.log(result)); // third job / callback

虽然两个作业不能 运行 在一个代理(= 线程)上并行,但一个异步函数的作业可能 运行 在另一个作业之间。因此,两个异步函数可以 运行 concurrently.

现在谁产生这些异步事件?这取决于您在 async 函数中等待的内容(或者更确切地说:您注册的回调)。如果它是一个计时器 (setTimeout),则设置一个内部计时器,并且 JS-thread 继续执行其他作业,直到计时器完成,然后执行传递的回调。其中一些,尤其是在 Node.js 环境中 (fetchfs.readFile) 将在 内部 启动另一个线程。当线程完成时(通过事件),您只需交出一些参数并接收结果。

要获得真正的并行性,即 运行同时执行两个作业,需要多个代理。 WebWorkers 正是 - 特工。因此,WebWorker 中的代码 运行 是独立的(有自己的作业队列和执行器)。

代理可以通过事件相互通信,您可以通过回调对这些事件做出反应。当然,如果将回调包装到 Promises 中,您也可以 await 来自另一个代理的操作:

const workerDone = new Promise(res => window.onmessage = res);

(async function(){
    const result = await workerDone;
        //...
})();

长话短说:

JS  <---> callbacks / promises <--> internal Thread / Webworker

¹ 还为这种行为创造了其他术语,例如 事件循环/队列 等。术语 Job 由 ECMA262 指定。

² 引擎如何实现代理由引擎决定,但由于一个代理一次只能执行一个作业,因此每个代理有一个线程非常有意义。

Workers 也可以通过异步代码(即 Promises)访问,但是 Workers 是 CPU 密集型任务的解决方案,这些任务会阻塞 JS 代码 运行 所在的线程;即使这个 CPU 密集函数是异步调用的。

因此,如果您有像 renderThread(duration) 这样的 CPU 密集函数,并且如果您确实喜欢

new Promise((v,x) => setTimeout(_ => (renderThread(500), v(1)),0)
    .then(v => console.log(v);
new Promise((v,x) => setTimeout(_ => (renderThread(100), v(2)),0)
    .then(v => console.log(v);

即使第二个花费更少的时间完成它也只会在第一个释放 CPU 线程后调用。所以我们将首先在控制台上获得 1,然后是 2

然而,如果这两个函数在不同的 Worker 上 运行,那么我们期望的结果将是 21,因为它们可以同时 运行 而第二个完成并且 returns 一条消息更早。

因此,对于基本的 IO 操作,标准的单线程异步代码非常高效,并且需要使用 Worker 是因为需要使用 CPU 密集且可以分段(一次分配给多个 Worker)的任务,例如作为 FFT 等等。

异步函数与网络工作者或节点子进程无关——与那些不同,它们不是多线程并行处理的解决方案。

一个 async function 只是 1 个函数的语法糖 returning 一个 promise then() 链。

async function example() {
    await delay(1000);
    console.log("waited.");
}

一样
function example() {
    return Promise.resolve(delay(1000)).then(() => {
        console.log("waited.");
    });
}

这两个在行为上几乎没有区别。 await 的语义或根据承诺指定的语义,每个 async function 对其结果做 return 承诺。

1:语法糖在 if/else 或循环更难表达为线性承诺链,但它在概念上仍然相同。

Are such functions threaded in the same way as a function executed through setInterval is?

是的,async functions 运行 的异步部分作为标准事件循环中的(承诺)回调。上面示例中的 delay 将使用正常的 setTimeout 实现 - 包装在易于使用的承诺中:

function delay(t) {
    return new Promise(resolve => {
        setTimeout(resolve, t);
    });
}

我想对我的问题添加我自己的答案,根据我从所有其他人的答案中收集到的理解:

最终,除了 web worker 之外,所有回调都是美化的回调。异步函数中的代码、通过 promises 调用的函数、通过 setInterval 调用的函数等等——所有这些都在主线程中执行,使用类似于上下文切换的机制。根本不存在并行性。

真正的并行执行及其所有优点和缺陷,仅适用于网络工作者和网络工作者。

(可惜 - 我认为 "async functions" 我们终于得到了精简和 "inline" 线程)

这是一种调用标准函数作为工作程序的方法,可实现真正的并行性。这是一个在撒旦的帮助下用鲜血编写的邪恶黑客,可能有大量的浏览器怪癖可以破坏它,但据我所知它有效。

[constraints: 函数头必须像 function f(a,b,c) 一样简单,如果有的话结果,它必须经过 return 语句]

function Async(func, params, callback)
{ 
 // ACQUIRE ORIGINAL FUNCTION'S CODE
 var text = func.toString(); 


 // EXTRACT ARGUMENTS
 var args = text.slice(text.indexOf("(") + 1, text.indexOf(")")); 
 args     = args.split(",");
 for(arg of args) arg = arg.trim();


 // ALTER FUNCTION'S CODE:
 // 1) DECLARE ARGUMENTS AS VARIABLES
 // 2) REPLACE RETURN STATEMENTS WITH THREAD POSTMESSAGE AND TERMINATION
 var body = text.slice(text.indexOf("{") + 1, text.lastIndexOf("}")); 
 for(var i = 0, c = params.length; i<c; i++) body = "var " + args[i] + " = " + JSON.stringify(params[i]) + ";" + body;
 body = body + " self.close();"; 
 body = body.replace(/return\s+([^;]*);/g, 'self.postMessage(); self.close();');


 // CREATE THE WORKER FROM FUNCTION'S ALTERED CODE
 var code   = URL.createObjectURL(new Blob([body], {type:"text/javascript"}));
 var thread = new Worker(code);


 // WHEN THE WORKER SENDS BACK A RESULT, CALLBACK AND TERMINATE THE THREAD
 thread.onmessage =
 function(result)
 {
  if(callback) callback(result.data);

  thread.terminate();
 }

}

因此,假设您具有这种潜在的 cpu 密集型功能...

function HeavyWorkload(nx, ny) 
{
 var data = [];

 for(var x = 0; x < nx; x++)
 {
  data[x] = [];

  for(var y = 0; y < ny; y++)
  {
   data[x][y] = Math.random();
  }
 }

 return data;
}

...您现在可以这样称呼它:

Async(HeavyWorkload, [1000, 1000],
function(result)
{
 console.log(result);
}
);