wii3d logo

 

The Wii3D Project System

 

Concurrency

Asynchronous Methods

 

The C# model for event driven applications allows one to write up asynchronous methods to do background tasks that will fire events upon progress changed and task completion. For example, if you have worked with the WebClient user control, I am sure that you know what I am talking about.

The code below creates an instance of the WebClient class, subscribes to its event DownloadStringCompleted, and then calls the asynchronous DownloadStringAsync(Uri uri) method. The advantage of this asynchronous method invocation is that it allows the rest of the GUI to continue to function while the WebClient downloads its data. When the WebClient has finished downloading the contents of the URI, it fires its DownloadStringCompleted event, to which our code is subscribed and will respond to by writing the result to the console.

   1:  void DownloadString(string url)
   2:  {
   3:      var client = new WebClient();
   4:      client.DownloadStringCompleted += client_DownloadStringCompleted;
   5:      client.DownloadStringAsync(new Uri(url));
   6:  }
   7:   
   8:  void client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
   9:  {
  10:      Console.WriteLine("Downloaded: " + e.Result);
  11:  }

It is apparent that the use of asynchronous methods is limited to background tasks that will take some time to execute, such as network IO.

Part 1 – Defining the Task

Assume that we have some time consuming task that we want to do in the background. I have created a basic class called Program:

   1:  class Program
   2:  {
   3:      public void DoTask()
   4:      {
   5:          //A time-consuming task
   6:          for (var i = 0; i < 100; i++)
   7:          {
   8:              Console.WriteLine(i);
   9:              Thread.Sleep(50);
  10:          }
  11:      }
  12:   
  13:      public static void Main(string[] args)
  14:      {
  15:          var prog = new Program();
  16:          prog.DoTask();
  17:      }
  18:  }

The DoTask() method will take a long time – it will iterate through a list of integers, printing out each one and pausing for 50ms. If this method were called from a GUI, and the list were sufficiently long, the user interface would hang until the task completed.

Part 2 – Converting the Task into an Asynchronous Task

In order to run our task in asynchronously, we need several changes in our code:

Public, exposed components:

Private, hidden components

DoAsync Method

This method is the asynchronous method that will invoke our worker. The method cannot be called while another operation is under way.

   1:  public void DoAsync()
   2:  {
   3:      //Create the worker
   4:      DoWorkerDelegate worker = DoWorker;
   5:      //Create the async callback
   6:      AsyncCallback completedCallback = DoCompletedCallback;
   7:   
   8:      //Lock the sync object to prepare for the async operation
   9:      lock (pDoSync)
  10:      {
  11:          if (DoIsBusy)
  12:          {
  13:              //If the async operation is already underway, throw an error
  14:              throw new InvalidOperationException("Currently performing 'Do' operation");
  15:          }
  16:   
  17:          //Create the async operation
  18:          var async = System.ComponentModel.AsyncOperationManager.CreateOperation(null);
  19:          //Create the async context
  20:          var context = new DoAsyncContext();
  21:          //Create a variable to contain whether the operation has been cancelled
  22:          bool cancelled;
  23:   
  24:          //Start the worker
  25:          worker.BeginInvoke(async, context, out cancelled, completedCallback, async);
  26:   
  27:          //Set the async operation to busy
  28:          DoIsBusy = true;
  29:          //Set the async context to the created context
  30:          pDoAsyncContext = context;
  31:      }
  32:  }

IsBusy Property

This property is a boolean indicating whether the task is running or not.

   1:  public bool DoIsBusy { get; set; }

DoCompleted Event

This event is fired when the operation has completed. The completed event will be fired either after completing normally when the worker completes its work, or after it has been cancelled (the AsyncCompletedEventArgs contain a Cancelled property).

   1:  public event System.ComponentModel.AsyncCompletedEventHandler DoCompleted;
   2:   
   3:  protected virtual void OnDoCompleted(System.ComponentModel.AsyncCompletedEventArgs e)
   4:  {
   5:      if (DoCompleted != null)
   6:          DoCompleted(this, e);
   7:  }

DoProgressChanged Event

This event is fired when there is a progress update. The worker will fire this event when it has made progress (the ProgressChangedEventArgs contain ProgressPercentage and UserState properties). The UserState property is just an object that gets passed back from the worker method, and it is up to you to choose what you want to pass back – status description for a label in the status bar, the object being processed etc.

   1:  public event EventHandler<System.ComponentModel.ProgressChangedEventArgs> DoProgressChanged;
   2:   
   3:  protected virtual void OnDoProgressChanged(System.ComponentModel.ProgressChangedEventArgs e)
   4:  {
   5:      if (DoProgressChanged != null)
   6:          DoProgressChanged(this, e);
   7:  }

CancelDoAsync Method

The asynchronous task can be cancelled by calling this method. This does not cancel the task immediately, but rather marks it for cancellation. The worker will inspect the pDoAsyncContext field for a cancellation flag and will then end gracefully by firing the DoCompleted event.

   1:  public void CancelDoAsync()
   2:  {
   3:      //Lock the sync object to prepare for the cancellation of the async operation
   4:      lock (pDoSync)
   5:      {
   6:          //Check if the context is not null
   7:          if (pDoAsyncContext != null)
   8:          {
   9:              //The operation exists, flag the operation for cancellation
  10:              pDoAsyncContext.Cancel();
  11:          }
  12:      }
  13:      //Set the async operation to not busy
  14:      DoIsBusy = false;
  15:  }

Note: If there is no asynchronous task underway, this method does nothing.

DoWorker Method

This method does the actual work that we want to do. The worker needs to continuously check if it has been flagged for cancellation, notify subscribed listeners of its progress.

   1:  private void DoWorker(System.ComponentModel.AsyncOperation async, DoAsyncContext asyncContext, out bool cancelled)
   2:  {
   3:      //Assume that the operation has not been cancelled
   4:      cancelled = false;
   5:   
   6:      //TODO: Do action
   7:      for (var i = 0; i < 10; i++)
   8:      {
   9:          //Create the progress event arguments
  10:          var progressArgs = new System.ComponentModel.ProgressChangedEventArgs(
  11:              //TODO: Calculate the percentage completion of the task
  12:              (int)((100 * i) / 10.0),
  13:              //TODO: Pass an object back
  14:              i
  15:              );
  16:   
  17:          //Operation takes time
  18:          Thread.Sleep(50);
  19:   
  20:          //Notify any subscribed listeners of the async operation's progress change
  21:          async.Post(
  22:              e => OnDoProgressChanged((System.ComponentModel.ProgressChangedEventArgs)e),
  23:              progressArgs
  24:              );
  25:   
  26:          //Check if the async operation has been flagged for cancellation
  27:          if (asyncContext.IsCancelling)
  28:          {
  29:              //Set the state of the async operation to cancelled
  30:              cancelled = true;
  31:              return;
  32:          }
  33:      }
  34:  }

DoWorkerDelegate Delegate

This matches the DoWorker method signature.

   1:  private delegate void DoWorkerDelegate(System.ComponentModel.AsyncOperation async, DoAsyncContext asyncContext, out bool cancelled);

DoCompletedCallback Method

This method is called upon asynchronous task completion. It is responsible for ending the worker, disposing of unused objects and notifying any subscribed listeners of the task's completion.

   1:  private void DoCompletedCallback(IAsyncResult ar)
   2:  {
   3:      //Get the worker from the result
   4:      var worker = (DoWorkerDelegate)((System.Runtime.Remoting.Messaging.AsyncResult)ar).AsyncDelegate;
   5:      //Get the async state from the result
   6:      var async = (System.ComponentModel.AsyncOperation)ar.AsyncState;
   7:      //Create a variable to contain whether the operation has been cancelled
   8:      bool cancelled;
   9:   
  10:      //Stop the worker
  11:      worker.EndInvoke(out cancelled, ar);
  12:   
  13:      //Lock the sync object to prepare for async operation completion
  14:      lock (pDoSync)
  15:      {
  16:          //Set the async operation to not busy
  17:          DoIsBusy = false;
  18:          //Remove the reference to the async operation's context
  19:          pDoAsyncContext = null;
  20:      }
  21:   
  22:      //Create the completed event arguments
  23:      var completedArgs = new System.ComponentModel.AsyncCompletedEventArgs(null, cancelled, null);
  24:      //Notify any subscribed listeners of the async operation's completion
  25:      async.PostOperationCompleted(
  26:          e =$gt; OnDoCompleted((System.ComponentModel.AsyncCompletedEventArgs)e),
  27:          completedArgs
  28:          );
  29:  }

pDoAsyncContext Field and the DoAsyncContext Class

This field contains the context for the asynchronous task i.e. its cancellation information.

   1:  private DoAsyncContext pDoAsyncContext = null;
   2:   
   3:  internal class DoAsyncContext
   4:  {
   5:      private readonly object sync = new object();
   6:   
   7:      private bool isCancelling = false;
   8:      public bool IsCancelling
   9:      {
  10:          get { return isCancelling; }
  11:      }
  12:   
  13:      public void Cancel()
  14:      {
  15:          lock (sync)
  16:          {
  17:              isCancelling = true;
  18:          }
  19:      }
  20:  }

The pDoSync Field

This is used for synchronisation. It ensures that a task is not marked for cancellation during invocation or when the task is completing.

   1:  private readonly object pDoSync = new object();

Part 3 - Usage

The program shown below demonstrates the usage of the changes. 50% of the time that this runs, it should list 0 to 99 and then say "Completed", and the rest of the time list 0 to some value less than 99 and then say "Cancelled".

   1:  public static void Main(string[] args)
   2:  {
   3:      var prog = new Program();
   4:      //Subscribe to events
   5:      prog.DoCompleted += prog_DoCompleted;
   6:      prog.DoProgressChanged += prog_DoProgressChanged;
   7:      //Start the asynchronous method
   8:      prog.DoAsync();
   9:   
  10:      Thread.Sleep(new Random().Next(2500, 7500));
  11:      prog.CancelDoAsync();
  12:   
  13:      Console.Write("Press enter to exit");
  14:      Console.ReadLine();
  15:  }
  16:   
  17:  static void prog_DoProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
  18:  {
  19:      Console.WriteLine(e.UserState);
  20:  }
  21:   
  22:  static void prog_DoCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
  23:  {
  24:      Console.WriteLine(e.Cancelled ? "Cancelled" : "Completed");
  25:  }

<< Back to Project