13

私は最近 IAsyncResult に出くわしたばかりで、かなり長い間遊んでいます。私が実際に疑問に思っているのは、より優れた代替 ThreadPool があるのに、なぜ IAsyncResult を使用するのかということです。両方についての私の現在の理解から、私はほぼすべての状況で ThreadPool を使用することを選択します。だから私の質問は、 IAsyncResult が別のものよりも優先されるコンテキストはありますか?

私が IAsyncResult を好まない理由:

  • BeginXXX と EndXXX で複雑さを追加
  • 戻り値を気にしない場合、呼び出し元は EndXXX の呼び出しを忘れる可能性があります
  • API 設計の冗長性の増加 (非同期で実行するすべてのメソッドに対して、Begin および End ラッパー メソッドを作成する必要があります)
  • 可読性の低下

コードに入れるには:

スレッドプール

  public void ThreadPoolApproach()
  {
     ThreadPool.QueueUserWorkItem( ( a ) =>
     {
        WebClient wc = new WebClient();
        var response = wc.DownloadString( "http://www.test.com" );
        Console.WriteLine( response );
     } );
  }

IAsyncResult

  public void IAsyncResultApproach()
  {
     var a = BeginReadFromWeb( ( result ) =>
     {
        var response = EndReadFromWeb( result );
        Console.WriteLine( response );
     }, "http://www.test.com" );
  }

  public IAsyncResult BeginReadFromWeb( AsyncCallback a, string url )
  {
     var result = new AsyncResult<string>( a, null, this, "ReadFromFile" );

     ThreadPool.QueueUserWorkItem( ( b ) =>
     {
        WebClient wc = new WebClient();
        result.SetResult( wc.DownloadString( url ) );
        result.Complete( null );
     } );

     return result;
  }

  public string EndReadFromWeb( IAsyncResult result )
  {
     return AsyncResult<string>.End( result, this, "ReadFromFile" );
  }
4

3 に答える 3

17

いいえ、2 つのコード スニペットには大きな違いがあります。どちらも実際にスレッドプールを使用しますが、最初のものはもちろん明示的に使用します。2 番目のものは、はるかに目立たない (そして壊れた) 方法でそれを行います。IAsyncResult コールバックは、スレッドプール スレッドで実行されます。

スレッドプールは共有リソースです。大規模なプログラムでは、TP スレッドを多数使用します。独自のコードで明示的に使用するだけでなく、.NET Framework でもそれらを使用します。スレッドプールで実行されるコードの種類に関するガイダンスは、迅速に実行され、TP スレッドを待機状態にするブロッキング呼び出しを行わないコードであることです。ブロッキングは、非常に非効率な方法で非常に高価な操作リソースを使用し、TP スレッドを使用している可能性のある他のコードを台無しにします。スレッドプールの重要な部分はスケジューラであり、実行中のTP スレッドの数を、マシンが使用できる CPU コアの数に制限しようとします。

しかし、ブロッキングはまさに最初のスニペットで行っていることです。WebClient.DownloadString() は非常に遅いメソッドであり、インターネット接続または回線の反対側のサーバーよりも速く完了することはできません。実際には、潜在的に数分間TP スレッドを占有しています。ほとんど作業を行っておらず、Socket.Read() 呼び出しが完了するのを常に待機しています。有効な CPU コア使用率は、せいぜい数パーセントです。

これは、BeginXxxx() または XxxxAsync() メソッドを使用する場合とは大きく異なります。これは、オペレーティング システムに I/O 操作の開始を要求するコードとして内部的に実装されています。ほんの数マイクロ秒かかります。OS は要求をデバイス ドライバー (DownloadStringAsync() の場合は TCP/IP スタック) に渡します。I/O 要求キュー内のデータ項目として配置される場所。あなたの電話はすぐに戻ってきます。

最終的に、ネットワーク カードはサーバーからデータを取得し、ドライバーは I/O 要求を完了します。いくつかのレイヤーを介して、CLR が別の TP スレッドを取得し、コールバックを実行します。通常は数マイクロ秒かかる何らかの処理ステップで、データに対して行うことは何でもすばやく実行できます。

違いに注意してください。最初のコードは TP スレッドを数分占有していますが、非同期バージョンはスレッドをマイクロ秒分占有しています。非同期バージョンは、多くのI/O 要求を処理できるため、スケーリングがはるかに優れています。

コードの非同期バージョンの重大な問題は、正しく記述するのが非常に難しいことです。同期バージョンでローカル変数になるものは、非同期バージョンではクラスのフィールドになる必要があります。また、デバッグもはるかに困難です。これが、.NET が Task クラスを取得した理由であり、後に C# および VB.NET 言語での async/await キーワードのサポートによってさらに拡張されました。

于 2014-01-26T15:03:46.977 に答える
6

完了するのに専用スレッドを必要としない、本来非同期の IO バウンド操作を脇に置きましょう ( Stephen Cleary によるThere Is No Threadを参照してください)。プール スレッドで本来の非同期API の同期バージョンを実行してもあまり意味がありません。スレッドという貴重なリソースを無駄にブロックするからです。DownloadString DownloadStringAsync

代わりに、専用スレッドを必要とする CPU バウンドの計算操作に集中しましょう。

そもそも、.NET Framework には標準クラスがありません。AsyncResult<T>AsyncResult<string>コードで参照している 実装は、Jeffrey Richter によるConcurrent Affairs: Implementing the CLR Asynchronous Programming Model の記事から取ったものだと思います。また、著者はAsyncResult<T> 教育目的で実装する方法を示し、CLR 実装がどのように見えるかを示していると思います。彼は、プール スレッドで作業の一部を実行し、完了通知を提供するためにThreadPool.QueueUserWorkItem実装します。IAsyncResult詳細については、記事に付随するLongTask.csを参照してください。

したがって、質問に答えるには:

私が実際に疑問に思っているのは、より優れた代替 ThreadPool があるのに、なぜ IAsyncResult を使用するのかということです。

これは「IAsyncResultvs ThreadPool」のケースではありません。むしろ、あなたの質問の文脈では、IAsyncResultを補完するものThreadPool.QueueUserWorkItemであり、作業項目の実行が完了したことを発信者に通知する方法を提供します。ThreadPool.QueueUserWorkItemAPI 自体にはこの機能はありません。単に、bool作業項目がプール スレッドでの非同期実行のために正常にキューに入れられたかどうかを示すだけです。

ただし、このシナリオでは、実装または使用する必要はまったくありませんAsyncResult<T>ThreadPool.QueueUserWorkItemフレームワークでは、デリゲートのBeginInvokeメソッドを使用するだけで、デリゲートを非同期に実行しThreadPool、完了ステータスを追跡できます。これが、フレームワークがデリゲートの非同期プログラミング モデル (APM) パターンを実装する方法です。たとえば、 を使用して CPU バウンドの作業を実行する方法は次のとおりです。BeginInvoke

static void Main(string[] args)
{
    Console.WriteLine("Enter Main, thread #" + Thread.CurrentThread.ManagedThreadId);

    // delegate to be executed on a pool thread
    Func<int> doWork = () =>
    {
        Console.WriteLine("Enter doWork, thread #" + Thread.CurrentThread.ManagedThreadId);
        // simulate CPU-bound work
        Thread.Sleep(2000);
        Console.WriteLine("Exit doWork");
        return 42;
    };

    // delegate to be called when doWork finished
    AsyncCallback onWorkDone = (ar) =>
    {
        Console.WriteLine("enter onWorkDone, thread #" + Thread.CurrentThread.ManagedThreadId);
    };

    // execute doWork asynchronously on a pool thread
    IAsyncResult asyncResult = doWork.BeginInvoke(onWorkDone, null); 

    // optional: blocking wait for asyncResult.AsyncWaitHandle
    Console.WriteLine("Before AsyncWaitHandle.WaitOne, thread #" + Thread.CurrentThread.ManagedThreadId);
    asyncResult.AsyncWaitHandle.WaitOne();

    // get the result of doWork
    var result = doWork.EndInvoke(asyncResult);
    Console.WriteLine("Result: " + result.ToString());

    // onWorkDone AsyncCallback will be called here on a pool thread, asynchronously 
    Console.WriteLine("Press Enter to exit");
    Console.ReadLine();
}

最後に、APM パターンは、はるかに便利で適切に構造化されたTask-based Asynchronous Pattern (TAP)に取って代わられていることに言及する価値があります。他の低レベル API よりも TAP パターンを優先することをお勧めします。

于 2014-01-29T00:24:59.647 に答える
0

基本的に、これら 2 つの方法は、同じものの異なる動作です。ThreadPool で IAsyncResult を使用する理由の 1 つは戻り値です。Threading.WaitCallback は void を返すため、ThreadPool.QueueUserWorkItem 呼び出しで直接値を返すことはできませんが、IAsyncResult アプローチでは可能です。

于 2014-01-26T14:26:20.317 に答える