1. ホーム
  2. javascript

[解決済み] 非同期関数内でのawaitの使用について

2023-07-11 20:50:08

質問

私のコードのどこかにsetIntervalによって実行される非同期関数があります。この関数は、定期的にいくつかのキャッシュを更新します。

私はまた、値を取得する必要がある別の同期関数を持っています - できればキャッシュから、しかしそれがキャッシュミスであれば、データの起源から (IO 操作を同期方式で行うことは賢明でないと思いますが、この場合、これが必要であると仮定します)。

私の問題は、同期関数が非同期関数からの値を待つことができるようにしたいのですが、それは await キーワードの内部で、非 async 関数の中で使うことができます。

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

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

の中のロジックを取り出すことで、簡単に回避することができます。 updateCacheForKey の中のロジックを新しい同期関数に抽出し、この新しい関数を既存の両方の関数から呼び出すことで、簡単に回避できます。

私の疑問は、なぜこの使用例を最初に絶対に阻止するのかということです。私の唯一の推測は、ほとんどの場合、同期関数から非同期関数を待機することは間違っているので、quot;idiot-proofing" と関係があるということです。しかし、それは時に有効なユースケースを持っていると思うのは間違いでしょうか?

(私は、これはC#でも Task.Wait を使えば可能だと思います。)

どのように解決するのですか?

<ブロッククオート

私の問題は、同期関数が非同期関数からの値を待つことができるようにしたいのですが...。

できないんです、なぜなら。

  1. JavaScript はスレッドによって処理されるジョブキューに基づいて動作し、ジョブには実行から完了までのセマンティクスがあります。

  2. JavaScript は実際には非同期関数を持っていません。 async 関数でさえも、蓋を開けてみれば、プロミスを返す同期関数です。 を返す同期関数です (詳細は後述します)。

ジョブキュー (イベントループ) は概念的には非常にシンプルです。何かを行う必要があるとき (スクリプトの初期実行、イベント ハンドラのコールバックなど)、その作業はジョブ キューに入れられます。そのジョブキューを管理するスレッドは、次の保留中のジョブをピックアップし、それを完了するまで実行し、そして次のジョブのために戻ってくるのです。 (もちろんもっと複雑ですが、今回の目的ではこれで十分です)。 つまり、関数が呼び出されるときは、ジョブの処理の一部として呼び出され、次のジョブが実行される前に必ずジョブが完了するように処理されます。

完了まで実行するということは、ジョブが関数を呼び出した場合、その関数はジョブが完了する前に戻らなければならないことを意味します。スレッドが他のことをするために走り去る間、ジョブが途中で中断されることはありません。このため、コード を劇的に 他のことが起こっている間にジョブが途中で中断される場合よりも、正しく記述し、推論することがよりシンプルになります。 (繰り返しになりますが、これよりもっと複雑ですが、ここでの目的にはこれで十分です)。

ここまではいいとして。非同期関数を本当に持っていないとはどういうことだ!?

私たちは同期関数と非同期関数について話し、さらには async キーワードがありますが、関数の呼び出しは常に 同期 です。また async 関数は 同期的に は、関数のロジックが満たすか拒否するかの約束を返します。 後に というプロミスを返し、環境が後で呼び出すコールバックをキューに入れます。

と仮定しましょう。 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;
    });
}

(これについては、最近の私の本の第9章に詳しく書いてあります。 JavaScript。新しいおもちゃ .)

ブラウザにデータの取得処理を開始するよう依頼し、コールバックを登録します(via. then を通して)コールバックを登録し、データが戻ってきたときにブラウザが呼び出すようにします。 そして終了します。 からプロミスを返します。 then . データはまだ取得されていませんが updateCacheForKey は終了しています。それは返されました。それは同期的に仕事をしたのです。

後に フェッチが完了すると、ブラウザはそのプロミス コールバックを呼び出すジョブをキューに入れます。そのジョブがキューからピックアップされると、コールバックが呼び出され、その戻り値がプロミスを解決するために使われます then が返されます。

私の疑問は、そもそもなぜこのユースケースを絶対に阻止するのか、ということです。

どのようなものか見てみましょう。

  1. スレッドはある仕事をピックアップし、その仕事には syncFunc を呼び出し、それが updateCacheForKey . updateCacheForKey はブラウザにリソースを取得するよう依頼し、そのプロミスを返します。この非同期の魔法によって await のマジックによって、私たちは同期的にそのプロミスが解決されるのを待ち、ジョブを保持します。

  2. ある時点で、ブラウザのネットワークコードはリソースの取得を終了し、私たちが登録したプロミスコールバックを呼び出すためにジョブをキューに入れます。 updateCacheForKey .

  3. 何も起きない、もう二度と。)

...なぜなら、ジョブには run-to-completion セマンティクスがあり、スレッドは前のジョブを完了するまで次のジョブをピックアップすることが許可されていないからです。を呼び出したジョブを中断することは許されません。 syncFunc を呼び出したジョブを中断して、その約束を解決するジョブを処理することはできません。

これは恣意的に見えますが、繰り返しになりますが、その理由は、正しいコードを書くことと、コードが何をしているかについての推論が劇的に容易になるからです。

しかし、quot;synchronous"関数がquot;asynchronous"関数の完了を待つことができないことを意味しています。

そこには ロット のような、細部の手のひら返しがあります。細かいことを知りたいなら、仕様書を掘り下げるといい。