Reputation: 27713
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?
Upvotes: 3
Views: 480
Reputation: 10968
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
.
Upvotes: 6
Reputation: 11607
The Windows UI is not thread safe and does not support multi-threading at all. For this reason there is a check as to which thread creates and later tries to manipulate the allocated graphics resources. To avoid the exception you MUST use the invoke pattern shown here:
if(InvokeRequired)
{
Invoke(worker_RunWorkerCompleted, sender, e);
}
else
{
MessageBox.Show(f1, "Inside RunWorkerCompleted");
}
The fact that a different thread runs the method is normal. The Windows Forms are constructed by the entrant thread which must be re-entrant, this means you should not block (infinitely loop) the thread that runs the program at first.
If you look closely, in Main()
is a Run()
method somewhere. This is done so that the creating thread is free to terminate while the form goes on living his own life on the desktop.
Upvotes: 0