A pattern for “cancellable” background task

While working on Nym’s fancy downloader I stumbled upon a common problem: a task (the download to be precise) going on in a background which needs to be cancelled. Usually this sort of task is implemented by means of a loop executed on a background thread which periodically checks a “cancelled” flag. Exactly as illustrated below:

Code Snippet
  1. public void DoWork()
  2. {
  3.     while ( !Cancelled )
  4.     {
  5.         // Problem if DoSomeStuff() takes long time to complete...
  6.         DoSomeStuff();
  7.     }
  8. }

 

This is pretty much how the BackgroundWorker goes about it’s business. The only difference is that Cancelled and DoSomeStuf() are replaced by event’s. The major problem with this approach is the fact that when you want to cancel such a loop and wait for the thread to actually finish it’s job, you may be in for a very long wait. The method being executed on the background thread prevents it from checking Cancelled flag and thus terminating. In the context of the “fancy downloader” the problem was the method which actually reads the network stream and saves it to the file on your hard drive: if  it blocks due to poor network performance, busy site, etc the user would be facing a “hang”  in the app. The alternative solution is presented below:

Code Snippet
  1. protected virtual void CopyStream()
  2. {
  3.     var buffer = new byte[BlockSize];
  4.  
  5.     IAsyncResult result = Source.BeginRead(buffer, 0, BlockSize, null, null);
  6.  
  7.     while (WaitHandle.WaitAny(new[] { result.AsyncWaitHandle, _haltEvent }) == 0)
  8.     {
  9.         int bytesRead = Source.EndRead(result);
  10.  
  11.         if (bytesRead == 0)
  12.             return;
  13.  
  14.         Destination.Write(buffer, 0, bytesRead);
  15.  
  16.         result = Source.BeginRead(buffer, 0, BlockSize, null, null);
  17.     }
  18. }
  19.  
  20. public virtual void Cancel()
  21. {
  22.     _haltEvent.Set();
  23.     Thread.Join();
  24. }

In this case the loop simply continues as long as one of the two wait handles are signalled. If the first one gets signalled as the result of async I/O being completed, the program writes the data to the file. If the second handle gets signalled (due to Cancel method being called) the thread terminates. This allows for a Cancel() method to be pretty much instantaneous. The same approach can be used as well with any other operation but delegates may need to be used to execute method in question. What we also need to remember is the fact that the background operation does not simply “go away” because of our task being cancelled, so it would be wise to test the program and ensure that it does not crash because of some resources being disposed etc.

March 27 2010
blog comments powered by Disqus