2
\$\begingroup\$

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 print Done 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 of Native.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 of AsyncMethod(); 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.
\$\endgroup\$
5
  • \$\begingroup\$ First thing, change your code to use the async/await pattern correctly, in this platform. The Button.Click handler must be marked async. Then, if you want t span a number of Tasks, build a collection and await Task.WhenAll() (i.e., async / await to the root of the calls). Don't use Thread.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\$
    – Jimi
    Commented Feb 21 at 23:05
  • 1
    \$\begingroup\$ RegisterWindowMessage (should be WindowsForms12_ThreadCallbackMessage in your versione of .NET), in Control.Invoke(), is used for the notifications related to the callbacks. See the implementation of WaitForWaitHandle() 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 of InvokeMarshaledCallbacks() \$\endgroup\$
    – Jimi
    Commented Feb 21 at 23:13
  • 1
    \$\begingroup\$ Because that's how the async / await pattern works (works as intended). The Click handler needs to be marked async, so StartAsyncCalls() must be async Task StartAsyncCalls() and AsyncMethod() cannot be async 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\$
    – Jimi
    Commented Feb 22 at 10:24
  • 1
  • \$\begingroup\$ Jimi is right about the async/await best practices. But if we just want to specifically check if the async/await will use the window message under the hood in the particular use case, then the initial code is pretty good. It has async/await (so the state machine is created) and real async 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 call RegisterWindowMessage("WindowsForms12_ThreadCallbackMessage") and check if the messages received by the form match to the result of this call. \$\endgroup\$
    – Serg
    Commented Feb 22 at 15:45

1 Answer 1

2
\$\begingroup\$

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.