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.
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\$