繁体   English   中英

在非异步函数中使用“await”

[英]Using "await" inside non-async function

我有一个异步函数,它由我代码中某处的 setInterval 运行。 此函数定期更新一些缓存。

我还有一个不同的同步函数,它需要检索值——最好从缓存中检索,但如果是缓存未命中,则从数据源中检索(我意识到以同步方式进行 IO 操作是不明智的,但让我们假设在这种情况下这是必需的)。

我的问题是我希望同步函数能够等待来自异步函数的值,但不可能在非async函数中使用await关键字:

function syncFunc(key) {
    if (!(key in cache)) {
        await updateCacheForKey([key]);
    }
}

async function updateCacheForKey(keys) {
    // updates cache for given keys
    ...
}

现在,可以通过将updateCacheForKey内部的逻辑提取到一个新的同步函数中,并从两个现有函数中调用这个新函数来轻松规避这一点。

我的问题是为什么首先要绝对阻止这种用例? 我唯一的猜测是它与“防白痴”有关,因为在大多数情况下,等待来自同步函数的异步函数是错误的。 但是我认为它有时具有有效的用例是错误的吗?

(我认为这在 C# 中也可以通过使用Task.WaitTask.Wait ,尽管我可能会在这里混淆一​​些东西)。

我的问题是我希望同步函数能够等待来自异步函数的值...

他们不能,因为:

  1. JavaScript 基于线程处理的“作业队列”工作,其中作业具有运行到完成语义,并且

  2. JavaScript 并没有真正的异步函数(真的 - 坚持我这个......)

作业队列(事件循环)在概念上非常简单:当需要完成某件事时(脚本的初始执行、事件处理程序回调等),该工作被放入作业队列中。 为该作业队列提供服务的线程接收下一个挂起的作业,运行它直到完成,然后返回进行下一个作业。 (当然,它比那更复杂,但这对于我们的目的来说已经足够了。)因此,当一个函数被调用时,它作为作业处理的一部分被调用,并且在下一个作业可以运行之前,作业总是被处理完成。

运行到完成意味着如果作业调用了一个函数,则该函数必须在作业完成之前返回。 当线程跑去做其他事情时,作业不会在中间暂停。 这使得代码大大简化正确书写和推理相比,就业岗位可能会被停在中间,而别的东西发生。 (同样,它比那更复杂,但这对于我们这里的目的来说已经足够了。)

到目前为止一切顺利。 没有真正的异步函数是什么意思?!

尽管我们谈论“同步”与“异步”函数,甚至有一个async关键字可以应用于函数,但在 JavaScript 中函数调用始终是同步的。 异步函数实际上并不存在。 我们有同步函数,可以设置环境稍后(通过排队作业)在适当的时候调用的回调。

让我们假设updateCacheForKey看起来像这样:

async function updateCacheForKey(key) {
    const value = await fetch(/*...*/);
    cache[key] = value;
    return value;
}

在幕后,它真正在做的是:

function updateCacheForKey(key) {
    return fetch(/*...*/).then(result => {
        const value = result;
        cache[key] = value;
        return value;
    });
}

它要求浏览器开始获取数据的过程,并向它注册一个回调(通过then ),以便浏览器在数据回来时调用,然后退出,从then返回承诺。 尚未获取数据,但updateCacheForKey已完成。 它已经回来了。 它同步完成了它的工作。

稍后,当获取完成时,浏览器将作业排队以调用该承诺回调; 当从队列中提取该作业时,将调用回调,其返回值用于解析承诺, then返回。

我的问题是为什么首先要绝对阻止这种用例?

让我们看看它会是什么样子:

  1. 该线程获取一个作业,该作业涉及调用syncFunc ,后者调用updateCacheForKey updateCacheForKey要求浏览器获取资源并返回其承诺。 通过这种非异步await的魔力,我们同步等待该承诺得到解决,从而阻止了工作。

  2. 在某个时候,浏览器的网络代码完成了对资源的检索,并将作业排队以调用我们在updateCacheForKey注册的承诺回调。

  3. 什么都没有发生,再一次。 :-)

...因为作业具有 run-to-completion 语义,并且线程在完成前一个作业之前不允许接受下一个作业。 该线程不允许在中间挂起调用syncFunc的作业,因此它可以处理将解决承诺的作业。

这似乎是随意的,但同样,这样做的原因是它使编写正确的代码和推断代码正在做什么变得更加容易。

但这确实意味着“同步”函数不能等待“异步”函数完成。

上面有很多细节等等。 如果你想深入了解它的本质,你可以深入研究规范。 带上很多食物和保暖的衣服,你会得到一些时间。 :-)

您可以通过立即调用函数表达式 (IIFE)异步函数中调用异步函数:

(async () => await updateCacheForKey([key]))();

并适用于您的示例:

function syncFunc(key) {
   if (!(key in cache)) {
      (async () => await updateCacheForKey([key]))();
   }
}

async function updateCacheForKey(keys) {
   // updates cache for given keys
   ...
}

现在,通过将 updateCacheForKey 中的逻辑提取到一个新的同步函数中,并从两个现有函数中调用这个新函数,可以很容易地避免这种情况。

TJ Crowder完美地解释了 JavaScript 中异步函数的语义。 但在我看来,上面的段落值得更多讨论。 根据updateCacheForKey作用,可能无法将其逻辑提取到同步函数中,因为在 JavaScript 中,有些事情只能异步完成。 例如,无法同步执行网络请求并等待其响应。 如果updateCacheForKey依赖于服务器响应,则无法将其转换为同步函数。

即使在异步函数和承诺出现之前也是如此:例如, XMLHttpRequest获取回调并在响应准备好时调用它。 无法同步获得响应。 Promise 只是回调的抽象层,异步函数只是 Promise 的抽象层。

现在,这可以以不同的方式完成。 在某些环境:

  • PHP 中,几乎一切都是同步的。 你用 curl 发送一个请求,你的脚本会阻塞直到它得到响应。
  • Node.js有其文件系统调用( readFileSyncwriteFileSync等)的同步版本,它们会阻塞直到操作完成。
  • 即使是普通的旧浏览器 JavaScript 也有alert和朋友( confirmprompt ),它们会阻止直到用户关闭模式对话框。

这表明 JavaScript 语言的设计者本可以选择XMLHttpRequestfetch等的同步版本。他们为什么不选择呢?

[W]为什么首先要绝对阻止这个用例?

这是一个设计决定。

例如, alert阻止用户与页面的其余部分交互,因为 JavaScript 是单线程的,并且唯一的执行线程会被阻塞,直到alert调用完成。 因此无法执行事件处理程序,这意味着无法进行交互。 如果有一个syncFetch函数,它会阻止用户做任何事情,直到网络请求完成,这可能需要几分钟,甚至几小时或几天。

这显然违背了我们称之为“网络”的交互环境的性质。 回想起来, alert是一个错误,除非在极少数情况下,否则不应使用它。

唯一的选择是在 JavaScript 中允许多线程,这是众所周知的难以编写正确程序的方法。 您是否对异步函数难以理解? 试试信号量!

可以向 async 函数添加一个很好的旧 .then() 并且它会起作用。

应该考虑而不是这样做,而是将当前的常规函数​​更改为异步函数,并一直向上调用堆栈直到不需要返回的承诺,即从异步函数返回的值没有任何工作要做。 在这种情况下,它实际上可以从同步调用。

这显示了函数如何既可以同步又可以异步,以及“立即调用函数表达式”习语如何仅在通过被调用函数的路径执行同步操作时才立即执行。

function test() {
    console.log('Test before');
    (async () => await print(0.3))();
    console.log('Test between');
    (async () => await print(0.7))();
    console.log('Test after');
}

async function print(v) {
    if(v<0.5)await sleep(5000);
    else console.log('No sleep')
    console.log(`Printing ${v}`);
}

function sleep(ms : number) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

test();

(基于Ayyappa在对另一个答案的评论中的代码。)

console.log 看起来像这样:

16:53:00.804 Test before
16:53:00.804 Test between
16:53:00.804 No sleep
16:53:00.805 Printing 0.7
16:53:00.805 Test after
16:53:05.805 Printing 0.3

如果您将 0.7 更改为 0.2,则一切都会异步运行:

17:05:14.185 Test before
17:05:14.186 Test between
17:05:14.186 Test after
17:05:19.186 Printing 0.3
17:05:19.187 Printing 0.4

如果您将两个数字都更改为超过 0.5,则一切都会同步运行,并且根本不会创建任何承诺:

17:06:56.504 Test before
17:06:56.504 No sleep
17:06:56.505 Printing 0.6
17:06:56.505 Test between
17:06:56.505 No sleep
17:06:56.505 Printing 0.7
17:06:56.505 Test after

不过,这确实暗示了原始问题的答案。 你可以有这样的功能(免责声明:未经测试的 nodeJS 代码):

const cache = {}

async getData(key, forceSync){
if(cache.hasOwnProperty(key))return cache[key]  //Runs sync

if(forceSync){  //Runs sync
  const value = fs.readFileSync(`${key}.txt`)
  cache[key] = value
  return value
  }

//If we reach here, the code will run async
const value = await fsPromises.readFile(`${key}.txt`)
cache[key] = value
return value
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM