I asked a question on Stack Overflow and there's a discussion between @Serg, who posted an answer and @Jimi, whose comments suggest that the answer might be wrong.
So I implemented the following code for demonstration purposes. It is a WinForms application written in C#. I'm running old fashioned .NET Framework 4.7.2, because that's what I know best.
From the Stack Overflow answer's link to Microsoft's implementation of async/await, we expect that .NET registers a Windows Message and uses that to delegate control to the UI thread.
In order to see that message, I implemented a message loop myself. I wanted to do it fully correctly, including TranslateAccelerator()
, but I failed on that one. Since my application doesn't use accelerators and the demo works without it, I felt that the program is "correct enough" and maybe that additional method would prevent me from seeing some messages, so perhaps it's even better.
The app is supposed to run in debug mode, since it uses Debug.Write()
to show what's going on. This is to avoid even more Window Messages.
My question is: does this program indeed show that the linked SO answer is correct and that .NET is posting messages to the UI thread? Or do I observe something else here?
Possible output in the debug window, 1 is the thread ID, C339 is the Window Message along with parameters. I'm doing 10 async calls and there are 10 messages received:
Running on 1
Running on 1
Running on 1
Running on 1
Running on 1
Running on 1
Running on 1
Running on 1
Running on 1
Running on 1
C339 0x00000000 0x00000000 0x007A06FE, 3391921
Done on 1
C339 0x00000000 0x00000000 0x007A06FE, 3392015
Done on 1
C339 0x00000000 0x00000000 0x007A06FE, 3392125
Done on 1
C339 0x00000000 0x00000000 0x007A06FE, 3392234
Done on 1
C339 0x00000000 0x00000000 0x007A06FE, 3392328
Done on 1
C339 0x00000000 0x00000000 0x007A06FE, 3392421
Done on 1
C339 0x00000000 0x00000000 0x007A06FE, 3392531
Done on 1
C339 0x00000000 0x00000000 0x007A06FE, 3392625
Done on 1
C339 0x00000000 0x00000000 0x007A06FE, 3392734
Done on 1
C339 0x00000000 0x00000000 0x007A06FE, 3392828
Done on 1
Class for the native P/Invoke stuff:
using System;
using System.Runtime.InteropServices;
using System.Windows.Interop;
namespace SO78028901MRE
{
internal static class Native
{
internal const int MaxIntAtom = 0xC000;
internal static readonly IntPtr AnyHWnd = IntPtr.Zero;
internal const uint NoFilter = 0;
[DllImport("user32.dll")]
internal static extern bool GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin = NoFilter, uint wMsgFilterMax = NoFilter);
[DllImport("user32.dll")]
internal static extern bool TranslateMessage([In] ref MSG lpMsg);
[DllImport("user32.dll")]
internal static extern IntPtr DispatchMessage([In] ref MSG lpMsg);
[DllImport("user32.dll")]
internal static extern bool TranslateAccelerator(IntPtr hWnd, IntPtr hAccTable, ref MSG lpMsg);
}
}
Form for testing purposes:
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Interop;
// Stack Overflow question 78028901
// Does async await use Windows Messages to return control to the UI thread?
// Minimum Reproducible Example to see Windows Messages arrive
namespace SO78028901MRE
{
/// <summary>
/// Form which will receive the expected async/await Windows Messages
/// </summary>
public partial class WMReceiveTestForm : Form
{
public WMReceiveTestForm()
{
InitializeComponent();
}
private void btnPerformTest_Click(object sender, EventArgs e)
{
StartAsyncCalls(10);
EndlessMessageLoop();
}
private void StartAsyncCalls(int count)
{
for (int i = 0; i < count; i++)
{
AsyncMethod();
Thread.Sleep(100);
}
}
//
/// <summary>
/// Prevent Windows Messages from being processed by some .NET code.
/// We want to see all messages ourselves.
/// </summary>
private static void EndlessMessageLoop()
{
while (Native.GetMessage(out var msg, Native.AnyHWnd))
{
// TODO: if (!Native.TranslateAccelerator(???, ???, ref msg))
{
if (IsRegisteredMessage(msg))
{
Debug.WriteLine("{0:X4} 0x{1:X8} 0x{2:X8} 0x{3:X8}, {4}",
msg.message, msg.lParam.ToInt32(), msg.wParam.ToInt32(), msg.hwnd.ToInt32(), msg.time);
}
Native.TranslateMessage(ref msg);
Native.DispatchMessage(ref msg);
}
}
}
/// <summary>
/// .NET should register a Windows Message via RegisterWindowMessage().
/// Such a message should have a message number above 0xC000.
/// This method determines if such a message was found.
/// </summary>
private static bool IsRegisteredMessage(MSG msg)
{
return msg.message > Native.MaxIntAtom;
}
/// <summary>
/// Simple method that can be called asynchronously and just waits.
/// Prints the thread ID before and after waiting.
/// </summary>
private async void AsyncMethod()
{
Debug.WriteLine("Running on " + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(2000).ConfigureAwait(true);
Debug.WriteLine("Done on " + Thread.CurrentThread.ManagedThreadId);
}
}
}
What else can we do with this code? (Not for review purposes, just FYI)
ConfigureAwait(false)
will printDone on X
, where X is a thread number, certainly one of the thread pool. In this case we don't see a message, because the task can easily be picked up by a threadpool thread without any message. IMHO, this confirms that the message is only sent if control needs to go to the UI thread.- Adding
if (!IsRegisteredMessage(msg))
in front ofNative.DispatchMessage(ref msg);
will successfully prevent the remaining code of the async method from running. IMHO this confirms that the message is related to the async execution. - Using
Task.Run(AsyncMethod);
instead ofAsyncMethod();
will begin execution on a threadpool thread rather than the UI thread. This makes.ConfigureAwait(true)
useless (at least if someone thinks that this makes it go back to the UI thread). Since it does not go back to the UI thread, there will also not be messages. But we should see the same thread IDs in the same order. - Combining
Task.Run(AsyncMethod);
and.ConfigureAwait(false)
may show different thread IDs when starting compared to ending. As expected.
1 Answer 1
Yes, your code is sufficient to show that async/await sends or posts Window Messages in order to get back to the UI thread.
Yet, you can still do better. As @Jimi pointed out in the comments, your code doesn't follow the async/await best practices [ 1, 2, 3, 4 ]. But that was certainly not the primary goal.
In particular, you would make the button click method async, and use Task.Delay()
instead of Thread.Sleep()
:
private async void btnPerformTest_Click(object sender, EventArgs e)
{
await StartAsyncCalls(10);
EndlessMessageLoop();
}
private async Task StartAsyncCalls(int count)
{
for (int i = 0; i < count; i++)
{
AsyncMethod();
await Task.Delay(100);
}
}
Only button click handlers may be async void
for backwards compatibility reasons. AsyncMethod()
should not be void
:
private async Task AsyncMethod()
{
Debug.WriteLine("Running on " + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(2000).ConfigureAwait(true);
Debug.WriteLine("Done on " + Thread.CurrentThread.ManagedThreadId);
}
In the .NET source code, you find that .NET uses RegisterWindowMessage(), and you can check exactly for that message only, instead of checking all messages in range 0xC000 through 0xFFFF:
Declare a P/Invoke and use the same message:
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern uint RegisterWindowMessage(string lpString);
[...]
_registeredMessage = Native.RegisterWindowMessage("WindowsForms12_ThreadCallbackMessage");
[...]
private bool IsRegisteredMessage(MSG msg)
{
return msg.message == _registeredMessage;
}
Now that you have proved that .NET indeed uses Window Messages, you still shouldn't blindly believe everything that @Serg said in the answer on Stack Overflow. Instead, get a debugger, put a conditional breakpoint in PostMessage()
and see what callstack you get.
You'll find that @Serg is correct about PostMessage()
being used, but he was wrong by saying that .Invoke()
is used. @Jimi is correct that .Invoke()
will not use PostMessage()
, but was wrong in the assumption that .BeginInvoke()
is not used.
Correct is: WindowsFormsSynchronizationContext will use .BeginInvoke()
and that will result in a PostMessage()
call. Hopefully, everyone has learned something from the question.
Conditional breakpoint for WinDbg (0xC339 is the message number; check it first). When the app is 64 bit, the second argument (msg
) will be in the EDX register:
bp user32!PostMessageW ".if (edx == 0xC339) {} .else {g}"
Call stack (return address and child stack removed for brevity):
0:011> k
# Call Site
00 USER32!PostMessageW
01 System_Windows_Forms_ni+0x3496e2
02 System_Windows_Forms_ni!System.Windows.Forms.Control.MarshaledInvoke+0x36f
03 System_Windows_Forms_ni!System.Windows.Forms.Control.BeginInvoke+0x62
04 System_Windows_Forms_ni!System.Windows.Forms.WindowsFormsSynchronizationContext.Post+0x51
05 mscorlib_ni!System.Threading.Tasks.AwaitTaskContinuation.RunCallback+0x6a [...\TaskContinuation.cs @ 759]
06 mscorlib_ni!System.Threading.Tasks.Task.FinishContinuations+0xfe [...\Task.cs @ 3642]
07 mscorlib_ni!System.Threading.Tasks.Task<System.Threading.Tasks.VoidTaskResult>.TrySetResult+0x65 [...\Future.cs @ 490]
08 mscorlib_ni!System.Threading.Tasks.Task.DelayPromise.Complete+0xad [...\Task.cs @ 5942]
09 mscorlib_ni!System.Threading.ExecutionContext.RunInternal+0x108 [...\executioncontext.cs @ 980]
0a mscorlib_ni!System.Threading.ExecutionContext.Run+0x15 [...g\executioncontext.cs @ 928]
0b mscorlib_ni!System.Threading.TimerQueueTimer.CallCallback+0x138 [...\timer.cs @ 713]
0c mscorlib_ni!System.Threading.TimerQueueTimer.Fire+0x91 [...\timer.cs @ 670]
0d mscorlib_ni!System.Threading.TimerQueue.FireNextTimers+0x78 [...\timer.cs @ 425]
0e clr!CallDescrWorkerInternal+0x83
- WindowsFormsSynchronizationContext.Post() calls
- Control.BeginInvoke(), which calls
- Control.MarshaledInvoke() which registers the message and calls PostMessage()
await Task.WhenAll()
(i.e., async / await to the root of the calls). Don't useThread.Sleep()
anywhere,Task.Delay()
when needed..ConfigureAwait(true)
is the default, not needed. The ManagedThreadID, without.ConfigureAwait(false)
, is of course the same -- WinForms uses RegisterWindwMessage() for different things (e.g., Mouse events that don't have a corresponding WM_SOMETHING) \$\endgroup\$WindowsForms12_ThreadCallbackMessage
in your versione of .NET), inControl.Invoke()
, is used for the notifications related to the callbacks. See the implementation ofWaitForWaitHandle()
in the source code. This message is sent to the the window procedure the control involved (but it doesn't bubble up). In this case, see the implementation ofInvokeMarshaledCallbacks()
\$\endgroup\$StartAsyncCalls()
must beasync Task StartAsyncCalls()
and AsyncMethod() cannot beasync void
(BAD).async void
in a synchronized platform requires the run-time to generate a different machinery to prevent the callback from being lost in the void. This is not the intended functionality, which may work in a scenario where there's no direct main-thread synch context, but the synch context is generated in the ThreadPool (e.g.,a Console app) \$\endgroup\$Delay
operation (so the control flow is really switching between the threads). For me, this is a good code for specific testing purposes. Additionally you can callRegisterWindowMessage("WindowsForms12_ThreadCallbackMessage")
and check if the messages received by the form match to the result of this call. \$\endgroup\$