Search code examples
c#threadpooliasyncresult

IAsyncResult vs ThreadPool


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" );
  }

Solution

  • 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.