Async/Aways is Not All You Need
async/await パターンは、C#、C++、Dart、Kotlin、Rust、Python、TypeScript/JavaScript、Swift など多くのプログラミング言語の機能となっています。これにより、非同期の非ブロッキング関数を、通常の同期関数に似た形で構造化することができます。
しかし、このパターンは複数の非同期タスクを同時に実行するには適していません。
たとえば、次の TypeScript コードは、TaskA と TaskB が独立しているにもかかわらず、順次実行されます。
const TaskRunner = async () => {
const a = await TaskA();
const b = await TaskB();
const c = await TaskC(a, b);
}
TaskA と TaskB を同時に実行するには、Promise.all を使用する必要があります。
const TaskRunner = async () => {
const [a, b] = await Promise.all(TaskA(), TaskB());
const c = await TaskC(a, b);
}
この方法は単純なケースでは問題ありませんが、次のような複雑なケースでは難しくなります(経験豊富な TypeScript 開発者であれば、続きを読む前に最適化を試してみてください)
const TaskRunner = async () => {
const a = await TaskA();
const b = await TaskB();
const c = await TaskC();
const d = await TaskD(a, b);
const e = await TaskE(b, c);
return TaskF(d, e);
};
このクイズを X やいくつかの開発者フォーラムで開発者に試したところ、多くの開発者、特に経験豊富な開発者でも、次のような答えを出してきました
const TaskRunner = async () => {
const [a, b, c] = await Promise.all([TaskA(), TaskB(), TaskC()]);
const [d, e] = await Promise.all([TaskD(a, b), TaskE(b, c)]);
return TaskF(d, e);
};
これは元のコードよりもかなり性能が向上しますが、最適ではありません。TaskD は TaskC を待つ必要がないのに待ち、TaskE は TaskA を待つ必要がないのに待つことになります。
この問題を指摘したところ、一人の開発者が次のような答えを出しました。TaskD と TaskE の両方が TaskB の完了を待つ必要があることに気付いたのです。
const TaskRunner = async () => {
const promiseA = TaskA();
const promiseC = TaskC();
const b = await TaskB();
const AthenD = async () => {
const a = await promiseA;
return TaskD(a, b);
}
const CthenE = async () => {
const c = await promiseC;
return TaskE(b, c);
}
const [d, e] = await Promise.all([AthenD(), CthenE()]);
return TaskF(d, e);
}
これは完全に最適化されていますが、このスタイルのコードは非常に読みづらく、スケールしません。何十もの非同期タスクがある場合、最適なコードを書くのは不可能です。
この問題を解決するために、「データフロープログラミング」を提案します。タスクを非循環データフローグラフのノードとして扱い、ノード間の依存関係を記述します。
データフロープログラミングスタイルでは、コードは次のようになります
import { computed } from '@receptron/graphai_lite';
const ExecuteAtoF = async () => {
const nodeA = computed([], TaskA);
const nodeB = computed([], TaskB);
const nodeC = computed([], TaskC);
const nodeD = computed([nodeA, nodeB], TaskD);
const nodeE = computed([nodeB, nodeC], TaskE);
const nodeF = computed([nodeD, nodeE], TaskF);
return nodeF;
};
computed()
は Promise.all の薄いラッパーで(@receptron/graphai_liteで定義されています)、入力ノードの配列と非同期関数から「computed node」を作成します。
const nodeD = computed([nodeA, nodeB], TaskD);
は、nodeD
が taskD
を表すノードであり、nodeA
と nodeB
からのデータを必要とすることを示しています。
このスタイルでは、実行順序を指定する必要はありません。ノード間のデータ依存関係を指定するだけで、システムが適切な順序を自動的に判断し、独立したタスクを同時に実行します。