簡體   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