Inside BackgroundWorker

The BackgroundWorker is a reusable component that can be used in different contexts, but sometimes with unexpected results.

If you are like me, you have mostly used background workers while doing Windows Forms development due to the flexibility they offer for running a background task. They support cancellation and give events that signal progress updates and task completion.

When used in Windows Forms, these events (ProgressChanged and RunWorkerCompleted) get executed back on the UI thread where you can freely access your form controls.

However, the logic of the progress changed and worker completed events being invoked in the thread that started the background worker is not something you get directly from the BackgroundWorker, but instead from the fact that you are running in the context of Windows Forms.

Take the following example that illustrates the use of a worker in three different scenarios:

– Console Application or Windows Service;
– Windows Forms;
– WPF.

using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
using System.Windows.Threading;

class Program
{
    static AutoResetEvent Synch = new AutoResetEvent(false);

    static void Main()
    {
        var bw1 = new BackgroundWorker();
        var bw2 = new BackgroundWorker();
        var bw3 = new BackgroundWorker();

        Console.WriteLine("DEFAULT");
        var unspecializedThread = new Thread(() =>
        {
            OutputCaller(1);

            SynchronizationContext.SetSynchronizationContext(
                new SynchronizationContext());

            bw1.DoWork += (sender, e) => OutputWork(1);
            bw1.RunWorkerCompleted += (sender, e) => OutputCompleted(1);
            // Uses default SynchronizationContext
            bw1.RunWorkerAsync();
        });
        unspecializedThread.IsBackground = true;
        unspecializedThread.Start();

        Synch.WaitOne();

        Console.WriteLine();
        Console.WriteLine("WINDOWS FORMS");
        var windowsFormsThread = new Thread(() =>
        {
            OutputCaller(2);

            SynchronizationContext.SetSynchronizationContext(
                new WindowsFormsSynchronizationContext());

            bw2.DoWork += (sender, e) => OutputWork(2);
            bw2.RunWorkerCompleted += (sender, e) => OutputCompleted(2);
            // Uses WindowsFormsSynchronizationContext
            bw2.RunWorkerAsync();

            Application.Run();
        });
        windowsFormsThread.IsBackground = true;
        windowsFormsThread.SetApartmentState(ApartmentState.STA);
        windowsFormsThread.Start();

        Synch.WaitOne();

        Console.WriteLine();
        Console.WriteLine("WPF");
        var wpfThread = new Thread(() =>
        {
            OutputCaller(3);

            SynchronizationContext.SetSynchronizationContext(
                new DispatcherSynchronizationContext());

            bw3.DoWork += (sender, e) => OutputWork(3);
            bw3.RunWorkerCompleted += (sender, e) => OutputCompleted(3);
            // Uses DispatcherSynchronizationContext
            bw3.RunWorkerAsync();

            Dispatcher.Run();
        });
        wpfThread.IsBackground = true;
        wpfThread.SetApartmentState(ApartmentState.STA);
        wpfThread.Start();

        Synch.WaitOne();
    }

    static void OutputCaller(int workerId)
    {
        Console.WriteLine(
            "bw{0}.{1} | Thread: {2} | IsThreadPool: {3}",
            workerId,
            "RunWorkerAsync".PadRight(18),
            Thread.CurrentThread.ManagedThreadId,
            Thread.CurrentThread.IsThreadPoolThread);
    }

    static void OutputWork(int workerId)
    {
        Console.WriteLine(
            "bw{0}.{1} | Thread: {2} | IsThreadPool: {3}",
            workerId,
            "DoWork".PadRight(18),
            Thread.CurrentThread.ManagedThreadId,
            Thread.CurrentThread.IsThreadPoolThread);
    }

    static void OutputCompleted(int workerId)
    {
        Console.WriteLine(
            "bw{0}.{1} | Thread: {2} | IsThreadPool: {3}",
            workerId,
            "RunWorkerCompleted".PadRight(18),
            Thread.CurrentThread.ManagedThreadId,
            Thread.CurrentThread.IsThreadPoolThread);

        Synch.Set();
    }
}

Output:

//DEFAULT
//bw1.RunWorkerAsync     | Thread: 3 | IsThreadPool: False
//bw1.DoWork             | Thread: 4 | IsThreadPool: True
//bw1.RunWorkerCompleted | Thread: 5 | IsThreadPool: True

//WINDOWS FORMS
//bw2.RunWorkerAsync     | Thread: 6 | IsThreadPool: False
//bw2.DoWork             | Thread: 5 | IsThreadPool: True
//bw2.RunWorkerCompleted | Thread: 6 | IsThreadPool: False

//WPF
//bw3.RunWorkerAsync     | Thread: 7 | IsThreadPool: False
//bw3.DoWork             | Thread: 5 | IsThreadPool: True
//bw3.RunWorkerCompleted | Thread: 7 | IsThreadPool: False

As you can see the output between the first and remaining scenarios is somewhat different. While in Windows Forms and WPF the worker completed event runs on the thread that called RunWorkerAsync, in the first scenario the same event runs on any thread available in the thread pool.

Another scenario where you can get the first behavior, even when on Windows Forms or WPF, is if you chain the creation of background workers, that is, you create a second worker in the DoWork event handler of an already running worker. Since the DoWork executes in a thread from the pool the second worker will use the default synchronization context and the completed event will not run in the UI thread.

Advertisements

One thought on “Inside BackgroundWorker”

  1. We’re using WPF and we’re having some problems because the WorkCompleted event does not run on the main thread. Your post helps us in finding out what the problem might be.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s