How could this work?

I'm sure most of you know about BackgroundWorker, a new component in .NET 2.0, that allows UI code to run tasks asynchronously, without running into cross thread update issues. But what would happen if the thread that launches the background worker is not a UI thread i.e. it doesn't run a message pump?
The answer is quite surprising and caused a piece of working code like this


AutoResetEvent waitForWorker = new AutoResetEvent(false);
void Method()
{
BackgroundWorker worker = new BackgroundWorker();
worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_Completed)
worker.RunAsync();
waitForWorker.WaitOne();
}

void worker_Completed(...)
{
waitForWorker.Set();
}

to stop working after making a tiny modification like this

class MyForm : Form {}

AutoResetEvent waitForWorker = new AutoResetEvent(false);
void Method()
{
MyForm f = new MyForm();
BackgroundWorker worker = new BackgroundWorker();
worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_Completed)
worker.RunAsync();
waitForWorker.WaitOne();
}

void worker_Completed(...)
{
waitForWorker.Set();
}

This caused Method() to hang indefinitely, because worker_Completed never got to run. That makes sense, because the thread running method is currently blocked on a call to waitForWorker.WaitOne() and background worker would require that thread to be pumping messages to get worker_Completed to run on that thread. But how did it work before? The first code snippet also blocks on WaitOne(), yet worker_Completed does run and everything works.

This is where things get interesting. A peek into BackgroundWorker via Reflector revealed that it internally uses the new AsyncOperationManager class to deal with running its event handlers like ProgressChanged and RunWorkerCompleted on the thread that called RunAsync. AsyncOperationManager is a general class that uses a SynchronizationContext object to do its stuff and it's SynchronizationContext that does the heavy lifting of running code on different threads. To get an instance of SynchronizationContext, AsyncOperationManager tries to access the SynchronizationContext.Current property, which retrieves the corresponding instance for the current thread.

The default behavior of the SynchronizationContext class is to simply delegate requests to run code on other threads to the threadpool. There are subclasses of SynchronizationContext, like System.Windows.Forms.WindowsFormsSynchronizationContext, that customize the behavior to do something different. WindowsFormsSynchronizationContext, for example, uses Control.BeginInvoke internally to marshal code to run on the thread that called RunAsync.

Can you see the problem now? Instantiating an instance of a Form derived class caused the SynchronizationContext of the current thread to change to WindowsFormsSynchronizationContext, which would use Control.BeginInvoke. And that would not work unless the target thread is pumping messages. The first sample worked because there was no specific SynchronizationContext available, so an instance of the base SynchronizationContext class itself is created. That would simply call ThreadPool.QueueUserWorkItem to run the event handlers, which obviously doesn't require the target thread to be pumping.

The lesson - be wary of using BackgroundWorker from non-UI threads, the behavior might change depending on objects previously instantiated on the thread.

 

Comments

# re: How could this work?

Thursday, November 15, 2007 8:44 AM by Peter Yung

Very Nice Work!

Quite helpful

Leave a Comment

(required) 
(required) 
(optional)
(required)