BackgroundWorker vs async/await

Edit: Well, that was embarassing. I missed the elementary step of blocking and releasing the controller thread via Task.Wait rather than the previous ManualResetEventSlim. That makes the code significantly less complex, and hence replacing the EAP pattern with the TAP pattern looks like a good improvement. The GitHub repository has now been updated.

I wanted to see the benefits and drawbacks of moving my reusable Windows service template from its current event-based asynchronous pattern (EAP) with the BackgroundWorker type to the more modern task-based asynchronous pattern (TAP) with async/await. The BackgroundWorker type still has its place, primarily with background operations behind a desktop UI. But there's no UI in a Windows service, so it's potentially a interesting place to explore the switch.

The switch-over is relatively simple. The original controller thread code is shown as the first code sample in this blog entry. Compare this with the new controller thread code below. This removes the BackgroundWorker ceremony and blocks immediately on the asynchronous task doing the real work, waiting for it to finish or crash.

_________________________________________________________________________________


// Invoked when the controller thread starts. 
private void ControllerThreadRunning()
{
    this.AppLog.Information("Controller thread has started.", "ServiceMain.ControllerThreadRunning");
 
     // And we're on our way.
    while ( !this.ServiceStopRequested )
    {
        // Start real work and then block until that finishes or crashes.
        var realWork = this.LaunchWorkAsync();
        realWork.Wait();
        // If SCM didn't request a service stop, the assumption is that 
        // the real work crashed and needs to be restarted.
        if ( !this.ServiceStopRequested )
        {
            this.PauseControllerThread("Pause before restarting work cycle.", this.RestartDelay);
        }
    }
 
    // This service will now stop.
    this.AppLog.Information("Service stopping at SCM request.", "ServiceMain.ControllerThreadRunning");
    this.Cleanup();
}
_________________________________________________________________________________ 

The original BackgroundWorker code and callbacks are shown in the fifth and sixth code samples in this blog entry. For comparison the new code that performs the service's real work is shown below.

Note that I've now implemented progress reporting via a lambda expression, and also I'm keeping the original method of notifying the service stop/shutdown message. This means that I haven't bothered with the normal cancellation token pattern. Perhaps most importantly, I've hinted (via Task.Factory.StartNew rather than the shorthand Task.Run) to the task scheduler that the service's real work will be long-lived. Long-lived tasks should result in the assignment of a dedicated thread rather than a thread from the threadpool. As we'll see in a moment, this is indeed what happens.

_________________________________________________________________________________

// This method handles all ceremony around the real work of this service.
private async Task LaunchWorkAsync()
{
    try 
    {
        // Setup progress reporting.
        var progressReport = new Progress<string>
            (progressInfo => { this.AppLog.Information(progressInfo, "ServiceMain.LaunchWorkAsync"); });
        var progress = progressReport as IProgress<string>;
        // Launch time.
        await Task.Factory.StartNew( () => this.DoWork(progress), TaskCreationOptions.LongRunning );
    }
 
    // Report any exception raised during the work cycle.
    catch (Exception ex)
    {
        this.AppLog.Error(string.Concat("Work cycle crashed", Environment.NewLine,
                                        ex.GetType().FullName, Environment.NewLine,
                                        ex.Message, Environment.NewLine,
                                        ex.StackTrace));
    }
 
    return;
}
 
// This is where this service's real work is done.
// The work cycles continuously until it's asked to stop.
// If the work crashes with an unhandled exception, the 
// controller thread will restart it after an appropriate delay.
private void DoWork(IProgress<string> progress)
{
    this.AppLog.Information("Work has started.", "ServiceMain.DoWork");
 
    while (!this.ServiceStopRequested)
    {
        Thread.Sleep(5000);     // Simulated work cycle.
        progress.Report("Completed current cycle of work.");
    }
}
_________________________________________________________________________________  

The new async/await code is definitely smaller and neater than the BackgroundWorker code, and removing the manual block/unblock of the controller thread is nice. Let's take a look at what happens when this new version of the service is run.

As expected, the SCM uses a dedicated (background) thread to perform the work cycles:

The progress reporting is done using threads from the threadpool:

Interestingly, the continuation after the work cycles are completed reuses the same dedicated thread: