この記事では、完全に非同期の計算モデルを導入することにより、javascriptの非同期機能に関する問題の解決策について説明します。 概念自体について説明し、実装への参照を示します。 猫の下で興味を持ってください。
はじめに
主なものから始めましょう-同期的または非同期的に? 同期計算プロセスと不要な非同期機能がある場合、これは問題です。 さらに、非同期がより有利であり、一般的にベストプラクティスである場合、この問題を解決する必要があります。
ソリューションにはすでに何がありますか? コールバックがあり、約束があります。 ただし、promiseは修正されたコールバックであり、問題を単純化するだけで、解決はしません。 問題の実際の解決策として、すべてを1つのモデル(完全に同期または完全に非同期)に減らす必要があります。
最新の標準では、独自の約束が現れてから、非同期/待機が発生しました。これにより、計算プロセスを完全に同期したモデルに減らすことができます。 そして、問題は解決されたように見えますが、この解決策について多くの苦情があります。
- 「優雅さ」とはかけ離れた実装
- 現在ゆっくり実行しています
- 読みにくいコードを生成する
- 「大量の」並行処理の編成にはあまり適していない(読みにくいコードの束なし)
あなたが批判する-申し出
async / awaitを忘れ、promiseを忘れましょう、どのように見えるべきでしょうか? 最小限の追加コードで、便利で一貫性がありますか? 関数のようなもの、非同期のみ:
- 定義が同期的であるように
- 動作原理が同期のようになるように
- 同期コンピューティングプロセス内で動作します
つまり、同期JS関数を非同期に変更するとよいでしょう。 このタスクを完了するためのステップのリストを作成しましょう。
- このような場合、最初に行うことは、独自の呼び出しスタックを導入することですが、単純な呼び出しスタックではなく、ツリーを導入することです! 従来の呼び出しスタックは、大規模な同時実行には適していません。
- 2番目はそうすることです。 そのため、ブランチの最上部は常に動作しているため、実行フローは、ブランチが終了した場合にそこに戻ることが保証されます。
- 第三に、これは関数の同期戻りの拒否であり、非同期代替との置き換えであり、結果を返す前に何かを待つことができます。 つまり、競合することのない実行順序ではなく、コマンドで戻る必要があります。
- 第4に、同期関数内の同期コンピューティングプロセスから離れることで、この関数の同期呼び出しは単に非同期プロセスをポンプでくみます。
- 5番目と最後に、これらは非同期呼び出し/戻りのラッパー関数であり、必要に応じてこれらをすべて配置する必要があります。 それらを
call
およびback
とcall
back
。
荒野へ!
それをすべて描写してみましょう。 条件により、非同期関数の定義は同期関数の定義とまったく同じでなければなりません。
function A () { }
最後から始める
call
みましょう:
- パラメーターがあります:現在の頂点、呼び出す関数名、この関数の引数
- このブランチの新しい頂点を作成し、現在へのリンクを残します
- 指定された引数で指定された関数を呼び出します
back
:
- パラメーター:現在の頂点、結果
- このブランチの現在の頂点を削除し、前の頂点へのリンクをたどります
- 前の頂点に指定された関数を呼び出し、現在の頂点の関数によって結果が返されます。
それでは、コールツリーを紹介しましょう
ここでは、最初の2つのポイントが実行されます。
- ある特定の初期ピーク、ツリーのルート、そしてすべての課題がそこから始まるようにします
- 呼び出された関数への引数として頂点が渡されるようにします。
- 呼び出された関数の唯一のパラメーターを頂点とし、この関数の現在の引数を含む必要なものはすべてその中に保存されます。
- 呼び出されると、このブランチの頂点は関数の実行コンテキストによってキャプチャされます
- 次に頂点がブランチのノードになると、新しい頂点は呼び出された新しい関数のコンテキストによってキャプチャされます
- ajaxなどの非同期機能では、頂点はクロージャーに格納されます
- 一般的に:現在の呼び出し->さらに呼び出し| 閉鎖で待っている| 帰る
function A ( top ) { }
同期復帰はもはや私たちにとって友達ではありません
ポイント3は、既に上記で説明したように、(コマンドによって)呼び出されると、特別なback
ラッピングメソッドが1つのノードを呼び出しツリーに戻ります。 したがって、非同期リターンが実行されます。 すぐに、同期戻り(戻り)が考慮されなくなり、非同期呼び出しが同期戻りの後でも存在し続けることに同意します。
すべてではない、それほど単純ではない
上記から、非同期に変換された特定の同期関数への呼び出しが1つではなくいくつかあることが明らかになります。
- 最初の呼び出し-実際には、古典的な関数呼び出し、
call
説明を参照 - 2番目以降-非同期サブコールから戻るには、説明を参照してください
特定の呼び出しごとに必要なコードをキャッチまたは分離し、そこで実行スレッドを転送する方法が必要です。 さらに、サブコールとリターンの間にデータを保存する機能が必要です。これは、各同期リターンの後に実行コンテキストが消えるためです。 以下を紹介します。
-
top
mark
を入力してください -
#
等しいmark
call
みましょう - 現在の頂点から関数名に等しい
mark
back
ます(削除されます)。 -
switch
に実行の流れを指示させます - 非同期で戻る(
back
)前に常にアクセスできるデータをtop.x = 1
直接書き込むようにします。
function A ( top ) { switch ( top.mark ) { case '#': break; case 'B': break; case 'C': break; } }
すべてを適切に配置する:
-
top.arg
渡された引数をcall
てtop.arg
- 渡された結果を
top.ret
function A ( top ) { switch ( top.mark ) { case '#': call(top, 'B', 'some_data'); break; case 'B': call(top, 'C', top.ret + 1); break; case 'C': back(top, {x: top.ret}); break; } }
上記の例から、通常の同期関数が拡張されているだけで、非同期アクションを待ってから戻ることができることがわかります。 また、ステップへの分割とそれらの順次実行に気付くことができます。 これと、スタックではなく呼び出しツリーが使用されているという事実を考慮し、並列性を追加します。
-
top
にsize
フィールドを入力してください -
call
size
現在の頂点のsize
1つ増やしcall
- 前の頂点の
size
1だけ小さくしsize
(現在の頂点は破棄されます)
function A ( top ) { switch ( top.mark ) { case '#': top.buf = []; call(top, 'B', 'some_data1'); call(top, 'B', 'some_data2'); call(top, 'B', 'some_data3'); break; case 'B': top.buf.push(top.ret); if ( !top.size ) { call(top, 'C', top.buf); } break; case 'C': back(top, top.ret); break; } }
関数を実行する一般的な非同期プロセスの任意の順次ステップで、大規模な並列タスクを起動できることがわかりました。 また、このタスクの完了を待って結果を蓄積する機会。 それを改善しましょう。つまり、特定の関数のtop.ret
結果は必ずしも必要ではないという事実を考慮し、1つのタスクで異なる関数を並行して実行できると便利です。
- 新しい
group
フィールドをtop
に入力します -
mark
をgroup['#name']
置き換えgroup['#name']
-
size
をgroup['#size']
置き換えgroup['#size']
- 新しいパラメーターgroupMarkを
call
で導入しcall
- 現在の頂点の
top.group[groupMark]
1ずつインクリメントしてcall
ます - 前の頂点の
top.group[groupMark]
1つ減らしtop.group[groupMark]
(現在の頂点は破棄されます)。 - 現在のグループの名前とサイズを含む特別な名前
top.group['#name']
およびtop.group['#size']
top.group['#name']
の値をcall
back
制御します
function A ( top ) { switch ( top.group['#name'] ) { case '#': top.buf = []; call(top, '#group1', 'B1', 'some_data1'); call(top, '#group1', 'B2', 'some_data2'); call(top, '#group1', 'B'3, 'some_data3'); break; case '#group1': top.buf.push(top.ret); if ( !top.size ) { call(top, '#group2', 'C', top.buf); } break; case '#group2': back(top, top.ret); break; } }
実行中のいくつかのグループが終了するまで待機する別の機会を追加しました。それだけです:)
-
top.group['##size']
にすべてのtop.group['#size']
top.group['##size']
の合計が含まれるようにします -
top.group['##name']
を入力して、この関数が呼び出されたグループの名前(戻りグループの名前)をtop.group['##name']
ます
function A ( top ) { switch ( top.group['#name'] ) { case '#': top.listB = []; top.listC = []; call(top, '#group1', 'B1', 'some_data1'); call(top, '#group1', 'B1', 'some_data1'); call(top, '#group1', 'B2', 'some_data2'); call(top, '#group2', '1', 'some_data1'); call(top, '#group2', '1', 'some_data1'); call(top, '#group2', '2', 'some_data2'); break; case '#group1': if (top.ret) {top.listB.push(top.ret);} if ( !top.group['##size'] ) { back(top, {B: top.listB, C: top.listC}); } break; case '#group2': if (top.ret) {top.listC.push(top.ret);} if ( !top.group['##size'] ) { back(top, {B: top.listB, C: top.listC}); } break; } }
まとめ
非同期関数の概念は上記で説明されており、完全に非同期の計算モデルを導入すると同時に、以下を可能にします。
- コードのシンプルさと秩序は維持されます
- 均一性保証
- 現時点では、作業の速度はasync / awaitよりもはるかに高速です(実装によって異なります)
- 並列計算を整理するための十分な機会
私は、並列コンピューティングに重点を置いて、 この概念のかなり成功した実装を開発しました。 重要な機能は、スレッド(WebWorker)のサポートと、どのスレッドであるかに関係なく、非同期的に関数を呼び出す機能です。