ispiro
ispiro

Reputation: 27713

Why is 'RunWorkerCompleted' executed on the wrong thread?

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

Answers (2)

Dirk
Dirk

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

pid
pid

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

Related Questions