Domanda

I've just come across IAsyncResult recently and have played with it for quite some time. What I'm actually wondering is why use IAsyncResult when we have a way better alternative ThreadPool there? From my current understanding about both of them, I would choose to use ThreadPool in almost every situation. So my question is, is there any context where IAsyncResult is preferred over another?

Why I do not prefer IAsyncResult:

  • Added complexity with the BeginXXX and EndXXX
  • Caller may forget calling EndXXX if he doesn't care about the return value
  • Increased redundancies in API design (we need to create Begin and End wrapper methods for every methods we want to run asynchronously)
  • Reduced readability

To put it in code:

ThreadPool

  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" );
  }
È stato utile?

Soluzione

No, there's a honking huge difference between your two code snippets. Both do in fact use the threadpool, the first one does it explicitly of course. The second one does it in far less visible (and broken) way, the IAsyncResult callback executes on a threadpool thread.

The threadpool is a shared resource, in a large program you'll have many uses for TP threads. Not just explicitly in your own code, the .NET Framework uses them as well. The guidance for the kind of code that runs on a threadpool is for it to be code that executes quickly and doesn't make any blocking calls that puts the TP thread into a wait state. Blocking is using a very expensive operating resource in a very inefficient way and gums up other code that might be using a TP thread. An important part of the threadpool is the scheduler, it tries to limit the number of executing TP threads to the number of CPU cores that the machine has available.

But blocking is exactly what you are doing in the first snippet. WebClient.DownloadString() is a very slow method that cannot complete any faster than your Internet connection or the server on the other end of the wire will allow. In effect, you are occupying a TP thread for, potentially, minutes. Not doing much of any work at all, it is constantly waiting for a Socket.Read() call to complete. Effective CPU core utilization is a few percent, at best.

That's much different when you use a BeginXxxx() or XxxxAsync() method. It is internally implemented as a bit of code to ask the operating system to start an I/O operation. Takes but a handful of microseconds. The OS passes the request on to a device driver, the TCP/IP stack in the case of DownloadStringAsync(). Where it will sit as a data item in an I/O request queue. Your call very quickly returns.

Eventually, your network card gets data from the server and the driver completes the I/O request. Through several layers, that gets the CLR to grab another TP thread and run your callback. You quickly do whatever you do with the data, some kind of processing step that normally takes microseconds as well.

Note the difference, your first code is occupying a TP thread for minutes, the async version ties up threads for microseconds. The async version scales much better, being capable of handling many I/O requests.

A significant problem with the asynchronous version of the code is that it is much harder to write correctly. What will be local variables in the synchronous version need to become fields of a class in the asynchronous version. It is also much harder to debug. Which is why .NET got the Task class, further extended later with the support for async/await keywords in the C# and VB.NET languages.

Altri suggerimenti

Let's set aside the naturally asynchronous IO-bound operations, which do not require a dedicate thread to complete (see There Is No Thread by Stephen Cleary). It doesn't make much sense to execute the synchronous version DownloadString of the naturally asynchronous DownloadStringAsync API on a pool thread, because your blocking a precious resource in vain: the thread.

Instead, let's concentrate on CPU-bound, computational operations, which do require a dedicated thread.

To begin with, there is no standard AsyncResult<T> class in the .NET Framework. I believe, the implementation of AsyncResult<string> you're referencing in your code was taking from the Concurrent Affairs: Implementing the CLR Asynchronous Programming Model article by Jeffrey Richter. I also believe the author shows how to implement AsyncResult<T> for educational purposes, illustrating how the CLR implementation might look like. He executes a piece of work on a pool thread via ThreadPool.QueueUserWorkItem and implements IAsyncResult to provide the completion notification. More details can be found in LongTask.cs, accompanying the article.

So, to answer the question:

What I'm actually wondering is why use IAsyncResult when we have a way better alternative ThreadPool there?

This is not a "IAsyncResult vs ThreadPool" case. Rather, in the context of your question, IAsyncResult is complementary to ThreadPool.QueueUserWorkItem, it provides a way to notify the caller that the work item has completed executing. The ThreadPool.QueueUserWorkItem API by itself doesn't have this feature, it simply returns bool indicating whether the work item has been successfully queued for asynchronous execution on a pool thread.

However, for this scenario, you don't have to implement AsyncResult<T> or use ThreadPool.QueueUserWorkItem at all. The Framework allows to execute delegates asynchronously on ThreadPool and track the completion status, simply by using delegate's BeginInvoke method. That's how the Framework implements the Asynchronous Programming Model (APM) pattern for delegates. For example, here's how you can perform some CPU-bound work using 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();
}

Finally, it's worth mentioning that the APM pattern is being superseded by a much more convenient and well-structured Task-based Asynchronous Pattern (TAP). It is recommended that TAP pattern should be favored over other, more low-level APIs.

Basically these two ways are just different behavior of the same thing. One of the reason to use IAsyncResult over ThreadPool is return value: Threading.WaitCallback returns void so you can't return any value directly by ThreadPool.QueueUserWorkItem calling but you can in IAsyncResult approach.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top