ペアのBeginXxx()/EndXxx()
呼び出しがコードに表示される場合、これは受け入れられます。 しかし、アルゴリズムが連続してそのような呼び出しをいくつか必要とする場合、メソッド(または匿名デリゲート)の数が増加し、コードが読みにくくなります。 幸いなことに、この問題はF#とC#の両方で解決されています。
挑戦する
したがって、完全に非同期モードでWebページをダウンロードし、ハードドライブに保存するとします。 このために必要です
- サイトページのダウンロードを開始
- ダウンロードしたら、ファイルのディスクへの書き込みを開始します
- 記録が完了したら、ファイルストリームを閉じてユーザーに通知します
シンプルなソリューション
問題の単純な解決策は次のようになります。
static void Main( string [] args)<br/>
{<br/>
Program p = new Program();<br/>
//
p.DownloadPage( "http://habrahabr.ru" );<br/>
// 10.
p.waitHandle.WaitOne(10000);<br/>
}<br/>
<br/>
//
private WebRequest wr;<br/>
private FileStream fs;<br/>
private AutoResetEvent waitHandle = new AutoResetEvent( false );<br/>
<br/>
//
public void DownloadPage( string url)<br/>
{<br/>
wr = WebRequest.Create(url);<br/>
wr.BeginGetResponse(AfterGotResponse, null );<br/>
}<br/>
<br/>
//
private void AfterGotResponse(IAsyncResult ar)<br/>
{<br/>
var resp = wr.EndGetResponse(ar);<br/>
var stream = resp.GetResponseStream();<br/>
var reader = new StreamReader(stream);<br/>
string html = reader.ReadToEnd();<br/>
// true
fs = new FileStream( @"c:\temp\file.htm" , FileMode.CreateNew,<br/>
FileAccess.Write, FileShare.None, 1024, true );<br/>
var bytes = Encoding.UTF8.GetBytes(html);<br/>
//
fs.BeginWrite(bytes, 0, bytes.Length, AfterDoneWriting, null );<br/>
}<br/>
<br/>
// , wait handle
private void AfterDoneWriting(IAsyncResult ar)<br/>
{<br/>
fs.EndWrite(ar);<br/>
fs.Flush();<br/>
fs.Close();<br/>
waitHandle.Set();<br/>
}<br/>
この決定はまだ小花です。 たとえば、ファイルからすべての画像を非同期でダウンロードして保存する必要があり、それらがすべて保存されている場合にのみ、それらが書き込まれたフォルダを開くと想像してください。 上記のパラダイムを使用すると、これは本当の悪夢です。
匿名代理人による解決策
最初にできることは、関数の断片を匿名のデリゲートにグループ化することです[ 1 ]。 次に、次のようになります。
private AutoResetEvent waitHandle = new AutoResetEvent( false );<br/>
public void DownloadPage( string url)<br/>
{<br/>
var wr = WebRequest.Create(url);<br/>
wr.BeginGetResponse(ar =><br/>
{<br/>
var resp = wr.EndGetResponse(ar);<br/>
var stream = resp.GetResponseStream();<br/>
var reader = new StreamReader(stream);<br/>
string html = reader.ReadToEnd();<br/>
var fs = new FileStream( @"c:\temp\file.htm" , FileMode.CreateNew,<br/>
FileAccess.Write, FileShare.None, 1024, true );<br/>
var bytes = Encoding.UTF8.GetBytes(html);<br/>
fs.BeginWrite(bytes, 0, bytes.Length, ar1 =><br/>
{<br/>
fs.EndWrite(ar1);<br/>
fs.Flush();<br/>
fs.Close();<br/>
waitHandle.Set();<br/>
}, null );<br/>
}, null );<br/>
}<br/>
このソリューションは適切に見えますが、呼び出しチェーンが非常に大きい場合、コードは読みにくくなり、管理が困難になります。 これは、たとえば、チェーンの実行を一時停止してキャンセルする必要がある場合に特に当てはまります。
非同期ワークフローを使用したソリューション
ワークフローはF#コンストラクトです。 アイデアは次のようなものです-特定のブロックを定義し、いくつかの演算子(たとえば、 let
など)を再定義します。 非同期ワークフローは、オペレータ( let!
do!
など)が再定義let!
れるワークフローであり、これらのオペレータが操作の完了を「待機」できるようにします。 つまり、書くとき
async {<br/>
⋮<br/>
let ! x = Y()<br/>
⋮<br/>
}<br/>
つまり、 Y
に対してBeginYyy()
を呼び出し、結果が利用可能になっBeginYyy()
、結果をx
書き込みます。
したがって、ファイルをダウンロードしてF#に書き込むのは、次のようになります。
// /
// F#
type WebRequest with <br/>
member x.GetResponseAsync() =<br/>
Async.BuildPrimitive(x.BeginGetResponse, x.EndGetResponse)<br/>
let private DownloadPage(url:string) =<br/>
async {<br/>
try <br/>
let r = WebRequest.Create(url)<br/>
let ! resp = r.GetResponseAsync() // let!
use stream = resp.GetResponseStream()<br/>
use reader = new StreamReader(stream)<br/>
let html = reader.ReadToEnd()<br/>
use fs = new FileStream( @"c:\temp\file.htm" , FileMode.Create,<br/>
FileAccess.Write, FileShare.None, 1024, true );<br/>
let bytes = Encoding.UTF8.GetBytes(html);<br/>
do ! fs.AsyncWrite(bytes, 0, bytes.Length) //
with <br/>
| :? WebException -> ()<br/>
}<br/>
// , -
Async.RunSynchronously(DownloadPage( "http://habrahabr.ru" ))<br/>
#な構文構造を使用して、F#を使用すると、特別に作成された「プリミティブ」( GetResponseAsync()
やAsyncWrite()
)を使用してBegin / Endセマンティクスで呼び出しを行うことができます。 奇妙なことに、C#でもほぼ同じことができます。
非同期列挙子を使用したソリューション
C#を介したCLRの有名な著者であるJeffrey Richterは、 PowerThreadingの著者でもあります 。 このライブラリ[ 2 ]は、多くの興味深い機能を提供します。そのうちの1つは、C#での非同期ワークフローアナログの実装です。
これは非常に簡単に行われますAsyncEnumerator
と呼ばれる一種の「トークンマネージャー」がAsyncEnumerator
ます。 このクラスを使用すると、実際にメソッドの実行を中断して再度実行できます。 メソッドの実行を中断するにはどうすればよいですか? これは、単純なyield return
を使用して行われます。
AsyncEnumerator
使用AsyncEnumerator
簡単です。 それをパラメーターとして取得してメソッドに追加し、戻り値をIEnumerator<int>
変更します。
public IEnumerator< int > DownloadPage( string url, AsyncEnumerator ae )<br/>
{<br/>
⋮<br/>
}<br/>
次に、 BeginXxx()/EndXxx()
を使用して3つの単純なルールを使用してコードをBeginXxx()/EndXxx()
ます。
- コールバックパラメーターとしての各BeginXxx()は
ae.End()
を受け取ります IAsyncResult
トークンとしての各EndXxx()は、ae.DequeueAsyncResult()を受け取りIAsyncResult
- 何かを待つ必要があるたびに、
yield return X
します。ここで、Xは開始された操作の数です
AsyncEnumerator
を使用する場合、ダウンロードメソッドは次のようにAsyncEnumerator
ます。
public IEnumerator< int > DownloadPage( string url, AsyncEnumerator ae)<br/>
{<br/>
var wr = WebRequest.Create(url);<br/>
wr.BeginGetResponse(ae.End(), null );<br/>
yield return 1;<br/>
var resp = wr.EndGetResponse(ae.DequeueAsyncResult());<br/>
var stream = resp.GetResponseStream();<br/>
var reader = new StreamReader(stream);<br/>
string html = reader.ReadToEnd();<br/>
using ( var fs = new FileStream( @"c:\temp\file.htm" , FileMode.Create,<br/>
FileAccess.Write, FileShare.None, 1024, true ))<br/>
{<br/>
var bytes = Encoding.UTF8.GetBytes(html);<br/>
fs.BeginWrite(bytes, 0, bytes.Length, ae.End(), null );<br/>
yield return 1;<br/>
fs.EndWrite(ae.DequeueAsyncResult());<br/>
}<br/>
}<br/>
ご覧のとおり、非同期メソッドは同期形式で記述されています。ファイルストリームに使用することさえできました。 yield return
をyield return
する追加の呼び出しをカウントしない限り、コードは読みやすくなりました。
あとは、このメソッドを呼び出すだけです。
static void Main()<br/>
{<br/>
Program p = new Program();<br/>
var ae = new AsyncEnumerator();<br/>
ae.Execute(p.DownloadPage( "http://habrahabr.ru" , ae));<br/>
} <br/>
おわりに
非同期呼び出しのチェーンの問題はそれほどひどいものではありませんでした。 単純な状況では、匿名のデリゲートが行います。 複雑なものの場合、非同期のワークフローとAsyncEnumerator
があり、どちらの言語が近いかによって異なります。
チェーンは単純ですが、ディペンデンシーグラフ全体はどうですか? それについて-次の投稿で。 ■
注釈
- ↑最初に個々のメソッドを作成し、ReSharperを使用してそれらを「インライン化」できることは注目に値します。
- ↑ライブラリは本当に面白いです。Reflectorで開くことをお勧めします。たくさんのおいしいものがあります。 また、ライブラリのライセンスではWindowsでのみ使用できるため、Monoファンを怒らせることに注意してください。