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.