In the following code, when the BackgroundWorker is launched, a SynchronizationContext does exist, but still, the RunWorkerCompleted handler is executed on a different thread than the RunWorkerAsync() and therefore throws an exception. Why?
And when the call to tempForm is removed it runs fine. (And the same for when substituting a MessageBox for a Form there.)
(The code shows a Form, launches a BackgroundWorker that references another Form f1 after one second, and then shows this second Form f1.)
public static Form1 f1;
static BackgroundWorker worker = new BackgroundWorker();
[STAThread]
static void Main()
{
worker.DoWork += worker_DoWork;
worker.RunWorkerCompleted += worker_RunWorkerCompleted;
f1 = new Form1();
using (Form1 tempForm = new Form1()) tempForm.ShowDialog();
//MessageBox.Show("A MessageBox won't cause the exception later. Only the Form does.");
if (SynchronizationContext.Current == null) throw new Exception("This is NOT thrown");
worker.RunWorkerAsync();
Application.Run(f1);
}
static void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
MessageBox.Show(f1, "Inside RunWorkerCompleted");
//Throws: Cross-thread operation not valid: Control '' accessed from a thread other than the thread it was created on.
}
static void worker_DoWork(object sender, DoWorkEventArgs e)
{
Thread.Sleep(1000);
}
Can anyone please explain what is going on here?
The problem is because you call the RunWorkerAsync from a default synchronization context. As a small example:
public static void Main()
{
var ctx1 = SynchronizationContext.Current; // returns null
var form = new Form();
var ctx2 = SynchronizationContext.Current; // returns a WindowsFormsSyncContext
form.ShowDialog();
var ctx3 = SynchronizationContext.Current; // returns a SynchronizationContext
worker.RunWorkerAsync(); // wrong context now
}
It appears that instantiating a form associates a WindowsFormsSynchronizationContext with the current thread. Interestingly after closing the form the associated synchronization context will be set to the default one, i.e. the one that uses the threadpool.
After some digging I found the reason for the - at a first glance - strange behaviour: the constructor of Control initializes the WindowsFormsSynchronizationContext if necessary (see reference source). Once you return from ShowDialog then there won't be any message loop, so SynchronizationContext.Current has to be reset, in this case to the default threadpool SynchronizationContext.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With