非同期性:なぜうまくいかないのですか?

非同期プログラムは書くのが面倒です。 「すべてが正しく非同期である」と宣言されたnode.jsでも、非同期関数の同じ同期類似物を追加するのは非常に不便です。 内部に複雑なコードを含むラムダを宣言できないPython構文について私たちに言えることは...



この問題の美しい解決策に特別なものは必要ないのは面白いですが、何らかの理由でまだ実装されていません。



問題の本質



このような同期コードがあるとしましょう:

var f = open(args);

checkConditions(f);

var result = readAll(f);

checkResult(result);






非同期のアナログはもっとひどく見えます:

asyncOpen(args, function (error, f){

if (error)

throw error;

checkConditions(f);

asyncReadAll(f, function (error, result){

if (error)

throw error;

checkResult(result);

});

});






呼び出しチェーンが長くなるほど、コードは悪化します。



たぶんあなたは十分に怖がっていませんか? 次に、すべての呼び出しを非同期に置き換えて、次のコードの類似物を作成してください。

while ( true )

{

var result = getChunk(args1);

while (needsPreprocessing(result))

{

result = preprocess(result);

if (!result)

result = obtainFallback(args2);

}

processResult(result);

}






そしてポイントは、ブナの木がたくさんかかるということではありません。 主な待ち伏せは上記の同期コードであり、読みやすいです。 しかし、あなたができる非同期の不名誉では、あなた自身でさえ数時間でそれを理解することができません。



ところで、この例は根拠がありません。 遠い類似性は何らかの形でpythonに実装する必要があり、非同期形式でした。



つるはしとスクラップソリューション



node.jsでPromiseのような概念を見たことがあるかもしれません。 だから、 彼女はもういません 。 最も一般的なコールバックは、はるかに人道的であることが判明しました。 したがって、Promiseについては説明しません。



そして、 Doライブラリについて説明ます。 このライブラリは、継続可能性の概念に基づいています。 アプローチの違いを示す例を次に示します。

// callback-style

asyncFunc(args, function (error, result){

if (error)

throw error;

doSomething(result);

});



// continuables-style

var continuable = continuableFunc(args);

continuable( function (result){ // callback

doSomething(result);

}, function (error){ // errback

throw error;

});



// continuables-style short

continuableFunc(args)(doSomething, errorHandler);






Continuableは、コールバックとエラーバックをパラメーターとして受け取り、非同期呼び出しを行う別の関数を返す関数です。



場合によっては、このアプローチはコードを大幅に簡素化できます-「継続可能スタイルのショート」を見てください。 ここでは、関数シグネチャが適切であるため、doSomethingをコールバックとして直接使用し、errbackとして、他の場所で定義されたある種の「標準」errorHandlerを使用します。



多くのことができます。 並列呼び出し、非同期マップ、その他の興味深いこと。 詳細については、記事「 コンボライブラリを実行する 」を参照してください 。 そこでは、コールバックスタイル(node.jsの標準)によってシャープ化された関数を継続可能スタイルに変換する方法について読むことができます。



ただし、最初の例に戻ります。 私たちの場合、どうすれば助けられますか? 実際には、これで:

Do.chain(

continuableOpen(args),

function (f){

checkConditions(f);

return continuableReadAll(f);

}

)( function (result){

checkResult(result);

}, errorHandler);






これは、最初の例の継続可能スタイルの類似物です。 まあ、おそらくコールバックスタイルよりも少し良いかもしれませんし、そうでないかもしれません。 少なくともチェーンの長さの増加に伴うインデントの増加は停止され、エラーハンドラーは1つのポイントに集中します。 しかし、元の4行の同期バージョンと比較すると、特にコードは恐ろしく見えます。 より複雑な例は、サイクルのあるものです-まったく厳しいものではありません。再びひどい庭を作らなければなりません。



急いで救助してください



つるはしとクローバーは役に立たなかった、私は何か崇高なものが欲しい。 非同期呼び出しは、同期呼び出しほど複雑ではないことを望みます。 そして理想的には-彼とほとんど違いはありませんでした。 そしてそれは可能です。



ソリューションインフラストラクチャは、Ivan Sagalaevの記事「 ADISP 」で最もよく説明されています。 ADISPは、幸福をもたらす彼が書いたPythonライブラリです。



同様の何かがJSでも収集できます。Er.jsは例として役立ちますが、最初の知り合いに多くの魔法をかけたので、Sagalaevの記事をお勧めします。



ADISPで使用されるアプローチにより、次のスタイルでコードを記述できます。

var func = process( function (){

while ( true )

{

var result = yield getChunk(args1);

while ( yield needsPreprocessing(result))

{

result = yield preprocess(result);

if (!result)

result = yield obtainFallback(args2);

}

yield processResult(result);

}

});






はい、これはループを使用した同じ恐ろしい例です。 すべての呼び出しは非同期です。 func wrapping関数は、装飾が必要であることを示すためにのみ提供されています。 プロセス-Sagalaevによって記述されたものと同様のデコレータ。 getChunk、needsPreprocessing、preprocess、obtainFallback、processResult-ADISPの用語で非同期デコレータによって装飾された非同期関数。



このアプローチは、Pythonスタイルに利回りがあればどこでも機能します。 つまり、V8はまだyieldをサポートしていないため、スパン内の優れた非同期node.jsです。



ネイティブソリューション



そのようなまともな結果を達成できる利回りトリックを使用する際に他に必要なものはありますか? 私はそう思う、なぜなら:



-非同期呼び出しのコンテキストでyieldキーワードを使用すると奇妙に見えます。 それでも、この言葉は他のいくつかのことを意図しています。

-フレーミング機能を装飾する必要性-不便さとエラーの追加の理由

-同じADISPのコードは、複雑ではありませんが、これがどのように機能するかを理解するには、頭をかなり壊す必要があります。 私はどういうわけかADISPを少し修正した形で使用しなければなりませんでした。 私は奇妙な振る舞いに出くわし、何が問題であるかを長く痛みをこめて掘り下げました。 傾斜は完全に別の場所にあることが判明しましたが、デバッグ中に夢中になる可能性は現実以上でした。



yieldの例は、ランタイムが便利で美しい方法で非同期呼び出しを実装するためのすべてを備えていることを明確に示しています。 内部メカニズムに触れなくても必要なことを達成できるように。 正当な質問-実装が複雑すぎてはならない組み込みのネイティブソリューションを提供してみませんか?



実際、ランタイムは非同期呼び出しの場所でコンテキストを記憶し、実行を停止し、コールバック呼び出しの時点で、すべてを正しい形式で復元し、必要に応じて例外をスローする必要があります。 そして、少なくとも潜在的にそれを行う方法を知っています。 問題は、このトリックが必要なときに彼女に伝える方法です。



ノンブロッキングライブラリ関数


原則として、非同期関数は、言語のコア(たとえば、setTimeout)またはライブラリ関数(たとえば、node.jsのfsモジュールの関数)に実装されます。 したがって、美しい非同期呼び出しの問題は、主にライブラリ関数に関連しています。



これは素晴らしいことを意味します-非同期ライブラリ関数に特別な規則を追加するだけで、美しい非同期呼び出しを行うことができます。 言語を変更する必要はありません。新しいキーワードを考え出す必要もありません。 ライブラリの作成者に、非同期ライブラリ関数が呼び出されたコンテキストを元に戻す方法が必要であり、現在の実行を停止する必要があることを示す方法を提供します。 このような関数は、たとえば次のように安全に使用できます。

while ( true )

{

doSomePeriodicTask();

nbSleep(1000);

}






ここで、nbSleepは、コールポイントでの実行を実際に中断し、いつか同じポイントから再開し、保存されたコンテキストをコールバックとして使用して、スリープする非ブロッキングコールです。



関数のペアさえ必要です-1つは通常のコールバック(結局、場合によってはコールバックのオプションが望ましい)で、2つ目は非ブロッキングです。 これは恐ろしいことではありません。必要に応じてラッパーを作成できます。

var asyncUnlink = fs.unlink;

fs.unlink = function (fName, callback){

if (callback)

return asyncUnlink(fName, callback);

return nbUnlink(fName);

};






少なくとも、トラブルを引き起こす可能性のある同期アナログは、FIGで捨てることができ、その使用を非ブロッキングアナログの使用に置き換えます。



非同期キーワード?


それでも言語レベルで美しい非同期呼び出しを追加したい場合は、明らかに、新しいキーワードなしではできません。 これがファンタジーであることは明らかです。実行環境を変更するのとは対照的に、言語を変更することは自由すぎることです。 それにもかかわらず、1つの目を見てみましょう、何が起こる可能性があります:

var result1 = async (callback, myAsyncFunc(args, callback)); // long form

var result2 = async myAsyncFunc(args); // short form

var result3 = async (cb, createTask(args, cb), function (task){TaskManager.register(task);});






-長い形式:コールバックは、非同期関数への転送のために戻りコンテキストが格納される変数の名前です

-短い形式:戻りコンテキストは最後の引数とともに追加されます

-3番目のオプションは「裏返し」です:戻り値はラムダに渡されます(作成された非同期「タスク」を特定の「マネージャー」に登録します-キャンセルしたいですか?)、そして非同期の通常の方法でコールポイントに戻ります



なぜ言語サポートが必要なのでしょうか? トリッキーなコールバックでこのようなことをする必要がある場合にのみ、私は思います。 たとえば、いくつかの機能にそれを与えます(ああ、練習がどれほど悪質か)。 ほとんどの場合、ライブラリ関数レベルでのサポートで十分です。 確かに、ノンブロッキングライブラリ関数の呼び出しは、asyncキーワードの優位性よりも良く見えるでしょう。



ちなみに、「裏返した」呼び出しのアイデアは、非ブロッキングライブラリ関数にも適用できます。現在の実行が終了する直前に呼び出す関数を関数に渡すだけで十分です。



最後に



いつかノンブロッキングライブラリ関数がV8とnode.jsに追加され、さらに非同期で美しくなることを願っています。 Pythonにも追加されることを願っています。 これが止まらず、同期関数の代わりに、すべての新しい、潜在的に愛される言語と環境で、非ブロッキング関数が存在することを願っています-それが理にかなっているところで



* Source Code Highlighter .






All Articles