まだ3番目の部分に到達し、最も興味深い部分である非同期コンピューティングの組織に到達しました。
最後の2つの記事では、 並列実行コードの抽象化とタスクハンドラーの協調実行について説明しました 。
次に、非同期タスクを処理する場合の制御フローを制御する方法を見てみましょう。
非同期性
同期操作-実行のスレッドをブロックした結果として結果を取得する操作。 単純な計算操作(数値の加算/乗算)の場合、これが唯一の方法です。入力/出力操作の場合はそれの1つです。たとえば、 「ファイルから100ミリ秒で何かを読み込もう」何もありません-これらの100ミリ秒間、実行のスレッドがブロックされます。
場合によっては、これは許容できます(たとえば、単純なコンソールアプリケーション、または目的をすべて実行することを目的とするユーティリティを作成する場合)が、そうでない場合もあります。 たとえば、UIが処理されるスレッドにあまりにも詰まっている場合、アプリケーションはフリーズします。 例に目を向ける必要はありません-サイト上のjavascriptがwhile(true);
、そのページの他のイベントハンドラは呼び出されなくなり、閉じる必要があります。 同じことは、UIイベントハンドラーでAndroidの下で何かの計算を開始すると(そのコードはUIストリームで呼び出されます)、ウィンドウが「アプリケーションが応答しません、閉じますか?」(そのようなウィンドウはwatchdog-timerによって呼び出されます)実行がUIシステムに戻るとリセットされます)。
非同期操作とは、何らかの操作を実行するように要求する操作であり、その実行のプロセス/結果を何らかの方法で追跡できます。 いつ完成するかは不明ですが、他のことを続けることができます。
イベントループ
イベントループは、キューからイベントを取得して何らかの形で処理する無限ループです。 そして、一定の間隔で-IOイベントが発生したかどうか、またはタイマーが期限切れになったかどうかを確認します -その後、イベントをキューに追加して後で処理します。
ブラウザの例に戻りましょう。 ページ全体が1つのイベントループで機能し、ロードされたjavascriptページがキューに追加されて実行されます。 ページ上でUIイベントが発生した場合(ボタンのクリック、マウスの移動など)-ハンドラーのコードがキューに追加されます 。 ハンドラーは順番に実行され、コードが動作している間は並列処理は行われません。他のすべてのユーザーが待機しています。 コードがsetTimeout(function() { alert(42) }, 5000)
ような特別な関数を呼び出す場合、これはループ外のどこかにタイマーを作成します。その後、 alert(42)
付きの関数コードalert(42)
キューに追加されます alert(42)
。
チップ:キュー内の誰かがハンドラーを実行する前に長い間何かを計算すると、タイマーハンドラーは明らかに5秒以内に実行されます。
2番目のトリック:たとえば、1ミリ秒の待機を要求した場合でも、さらに多くのパスが可能です。 イベントループの実装は、「ええ、キューは空です。最も近いタイマーは1ミリ秒です。1ミリ秒のIOイベントを待機します」。selectを呼び出すと、オペレーティングシステムの実装は次のようになります。とにかく、可能な限りコンテキスト切り替えを行っています」と、他のすべてのスレッドが利用可能な時間を常に使用していたため、私たちは飛びました。
選択する
低レベルの非同期IOイベントは、 選択のバリエーションを使用して実装されます 。 いくつかのファイル記述子があります(ファイル、ネットワークソケット、または何か(実際には、Linux上のファイル(またはその逆)、ファイルは何でもかまいません))。
そして、いくつかの同期関数を呼び出して、多くの記述子を渡し、そこから入力を期待するか、ストリームをブロックする何かを記述したい場合は、次のようにします。
- 提出した1つ以上の記述子は、希望する操作を完了する準備ができていません。
- タイムアウトが期限切れになりませんでした(設定されている場合)。
この手順の結果として、多くの読み取り/書き込みファイルを取得します。
コールバック
非同期操作の結果を取得する最も簡単な方法は、非同期操作を作成するときに、結果の実行/準備が進んでいるときに呼び出される関数にリンクを渡すことです。
これはかなり低レベルのアプローチであり、多くの場合、匿名関数の乱用に加えて関数を「列に」書くことができないと、「コールバック地獄」になります(4から10レベルの関数を入れ子にして順次操作を処理する状況):
// function someAsync(a, callback) { anotherAsync(a, function(b) { asyncAgain(b, function(c) { andAgain(b, c, function(d) { lastAsync(d, callback); }); }); }); } // function someAsync2(a, callback) { var b; anotherAsync(a, handleAnother); function handleAnother(_b) { b = _b; asyncAgain(b, handleAgain); } function handleAgain(c) { andAgain(b, c, handleAnd); } function handleAnd(d) { lastAsync(d, callback); } }
非同期モナド
私たちプログラマーは、さまざまな困難/ルーチンを隠すために抽象化し、一般化するのが大好きです。 したがって、とりわけ、非同期コンピューティングの抽象化があります。
「計算」とは何ですか? これは、 AをBに変換するプロセスです。 同期計算をA→Bとして記述します。
非同期値とは何ですか? この約束は、将来的に何らかのT値を提供することです(これは成功した結果または間違いである可能性があります)。 これをAsync [T]として示します。
次に、 「非同期操作」はA → Async[T]
ようになります。ここで、 Aは操作を開始するために必要な引数です(たとえば、これはGET要求を行うURLです)。
非同期[T]の使用方法 彼に、データが利用可能になったときに呼び出されるコールバックを受け入れるrunメソッドを持たせます: Async[T].run : (T → ()) → ()
( Tを取る関数を受け入れ、何も返しません)。
それでは、最も重要なこと、つまり非同期操作を継続する機能を追加しましょう。 Async [A]がある場合、 Aが利用可能になると、明らかにAsync [B]を作成し、その結果を待つことができます。 このような継続の関数は次のようになります。
Async[A].then : (A → Async[B]) → Async[B]
つまり 特定のAからAsync [B]を作成でき、いつかAを提供するAsync [A]があれば、 Async [B]をすぐに提供できるので問題ありません。その後、時間と最終的にすべてが収束します。
function Async(starter) { this.run = function(callback) { starter(callback); }; var runParent = this.run; this.then = function(f) { return new Async(function(callback) { runParent(function(x) { f(x).run(callback); }); }); }; }
そして、上記の合成例は次のようになります。
function someAsync(a) { return anotherAsync(a).then(function(b) { return asyncAgain(b).then(function(c) { return andAgain(b, c); }).then(function(d) { return lastAsync(d); }); }); }
しかし、より興味深い。 非同期値のタイプとエラー/結果を明示的に区別します。 現在、私たちは常にAsync [E + R]を持っています (さらに、これはタイプサムであり、2つのうちの1つです)。 そして、たとえば、 Async[E + R].success : (R → Async[E + N]) → Async[E + N]
導入できますAsync[E + R].success : (R → Async[E + N]) → Async[E + N]
Eは変更されていないことに注意してください。
このメソッドを実装できるのは、成功した結果を受け取った場合(つまり、 EではなくRを取得した場合)に渡された関数のみを実行し、次の非同期操作を開始する場合のみです。そうでない場合、非同期操作の結果は「誤った」ままです
this.success = function(f) { return new Async(function(callback) { runParent(function(x) { if (x.isError()) callback(x); else f(x).run(callback); }); }); };
ここで、 successメソッドを使用して非同期操作をチェーンすると、成功したイベント開発ブランチのみを処理し、エラーは後続のすべてのハンドラーをすり抜けて、すぐにrunに渡されたコールバックに移動します 。
実行のスレッドを抽象化し、抽象化に例外をスローしました。 もう少しプレイすれば、エラーを別のエラーに変換したり、 成功した結果を返すことができる失敗メソッドを思いつくことができます。
約束(約束、約束)
Thenableインターフェースを記述する標準があります。 上記で説明したものとほぼ同じように機能しますが、 Promises / A +には非同期操作を開始する概念はありません。 Thenableが手元にあれば、どこかで既に何かが行われているので、実行結果をサブスクライブするだけです。 そして、成功した/失敗した分岐を処理するための2つのオプション機能を使用する1つのthenメソッドがあり、異なるメソッドはありません。
ここで味と色、両方のアプローチには長所と短所があります。
async / await-約束+コルーチン
promiseを使用するには、信じられないほどの量のラムダ関数を使用する必要があります。 これは、視覚的にかなりうるさく不快な場合があります。 これを何らかの形で改善する方法はありますか?
あります。
多くのエントリポイントを持つことができるコルーチンがあります。 そして、それが私たちに必要なものです。 コルーチンを使用して、 Async [E + R]を生成し、結果のRをそこに入力するか、例外Eを発生させます。 そして、禅が始まります:
function someAsync*(a) { var b = yield anotherAsync(a), c = yield asyncAgain(b), d = yield andAgain(b, c); yield lastAsync(d); }
次に、このコルーチンを取得し、 Asyncの場合は出口を取得し、最後のyieldの結果を考慮して、他のコルーチンを使用して実行する場合は、それらを再帰的に実行するような優れた「エグゼキューター」が必要です。
そして、 async / awaitは、 yieldの名前をawaitに変更し、関数を宣言する前にasyncを書き込む場合です。 まあ、時には(たとえばPythonの場合)、 yieldとawaitの両方が利用可能な 非同期ジェネレーターを見ることができます 。 その後、それらは同じコルーチンのように動作しますが、戻り/受け入れの間に内部非同期操作の結果を待機するため、それを持つ操作は非同期になります。
さて、これが私の一連の記事の終わりです。それらが誰かにとって有用であり、それ以上混乱しないことを望みます。