Reputation: 1444
We're developing a multithreaded game engine in C#, and we got the problem that we need the STAThread attribute (or set our threads to STA manually) to enable drag-and-drop support (AllowDrop can't be set without STA). However, when we enable STA and the update method takes longer than the draw method (as demonstrated below), the window does not behave properly any more - when it's clicked in the task bar, it doesn't minimize and maximize as you would expect it. The exact behaviour is different on different systems, I guess that some kind of race conditions come into play here.
Here's our test code:
[STAThread]
public static void Main()
{
Form form = new Form();
form.Show();
Barrier barrier = new Barrier(2);
Thread updateThread = new Thread(() => {
while (true)
{
barrier.SignalAndWait();
Thread.Sleep(30); //Update
barrier.SignalAndWait();
}
});
updateThread.Start();
while (true)
{
barrier.SignalAndWait();
Thread.Sleep(15); //Draw
barrier.SignalAndWait();
Application.DoEvents();
}
}
Upvotes: 4
Views: 288
Reputation: 942109
I recognize this problem, I debugged a very similar issue back in the Vista days in an unmanaged app. The problem is pretty obscure and is related to the very temperamental WM_ACTIVATEAPP message that you generate by clicking the taskbar button. Particularly when it is dispatched by a nested message loop and that loop does message filtering to allow only certain "safe" messages to be dispatched.
It is caused by the Barrier.SignalAndWait() call. That blocks the UI thread and that is illegal for an STA thread. The CLR does something about it, it pumps its own message loop while the underlying synchronization object is not signaled. If it is that loop that dispatches WM_ACTIVATEAPP, and the odds are pretty high because you block for 30 msec and only pump once, then it goes wrong. For some mysterious reason subsequent messages do not get dispatched. Almost certainly caused by the filtering done by that message loop. Otherwise hard to see, that code was never published and impossible to decompile.
It is fixable, but not easily. The workaround is to force the CLR to not pump this message loop. Which is okay because your waits are short. Something you can do by overriding the SynchronizationContext.Wait() method. Unfortunately that is hard to do, the WindowsFormsSynchronizationContext class is sealed so cannot be derived from to override its Wait() method. You need to visit the Reference Source and copy/paste the class into your code. Give it another name.
I'll give the short version of it, showing what you need to change:
using System.Runtime.InteropServices;
class MySynchronizationContext : SynchronizationContext {
public MySynchronizationContext() {
base.SetWaitNotificationRequired();
}
public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) {
return WaitForMultipleObjects(waitHandles.Length, waitHandles, false, millisecondsTimeout);
}
[DllImport("kernel32.dll")]
static extern int WaitForMultipleObjects(int nCount, IntPtr[] lpHandles,
bool bWaitAll, int dwMilliseconds);
}
And install the new synchronization provider with:
System.ComponentModel.AsyncOperationManager.SynchronizationContext =
new MySynchronizationContext();
In a nutshell, the SetWaitNotificationRequired() call tells the CLR that it should call your Wait() override instead of using its own Wait() implementation. And the Wait() override uses the OS' blocking wait without otherwise pumping. Worked well when I tested it and solved the problem.
Upvotes: 5