How to wait for WaitHandle while serving WPF Dispa

2019-01-15 10:33发布

问题:

Someone emailed me and asked if I have a version of WaitOneAndPump for WPF. The goal is to wait for a handle (similar to WaitHandle.WaitOne) and pump WPF Dispatcher events while waiting, on the same stack frame.

I really don't think an API like this should be used in any production code, either for WinForms or WPF (perhaps, besides for UI Automation). WPF doesn't expose an explicit version of WinForms' DoEvents, which is a very good design decision, given the fair share of abuse the DoEvents API has been taking.

Nevertheless, the question itself is interesting, so I'm going to take it as an exercise and post whatever I may come up with as the answer. Feel free to post your own version too, if interested.

回答1:

The version of WaitOneAndPump I've come up with uses DispatcherHooks Events and MsgWaitForMultipleObjectsEx, to avoid running a busy-waiting loop.

Again, using this WaitOneAndPump (or any other nested message loop variants) in the production code is almost always will be a bad design decision. I can think of only two .NET APIs which legitimately use a nested message loop: Window.ShowDialog and Form.ShowDialog.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;

namespace Wpf_21642381
{
    #region MainWindow
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.Loaded += MainWindow_Loaded;
        }

        // testing
        async void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);

            try
            {
                Func<Task> doAsync = async () =>
                {
                    await Task.Delay(6000);
                };

                var task = doAsync();
                var handle = ((IAsyncResult)task).AsyncWaitHandle;

                var startTick = Environment.TickCount;
                handle.WaitOneAndPump(5000);
                MessageBox.Show("Lapse: " + (Environment.TickCount - startTick));
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
    }
    #endregion

    #region WaitExt
    // WaitOneAndPump
    public static class WaitExt
    {
        public static bool WaitOneAndPump(this WaitHandle handle, int millisecondsTimeout)
        {
            using (var operationPendingMre = new ManualResetEvent(false))
            {
                var result = false;

                var startTick = Environment.TickCount;

                var dispatcher = Dispatcher.CurrentDispatcher;

                var frame = new DispatcherFrame();

                var handles = new[] { 
                        handle.SafeWaitHandle.DangerousGetHandle(), 
                        operationPendingMre.SafeWaitHandle.DangerousGetHandle() };

                // idle processing plumbing
                DispatcherOperation idleOperation = null;
                Action idleAction = () => { idleOperation = null; };
                Action enqueIdleOperation = () =>
                {
                    if (idleOperation != null)
                        idleOperation.Abort();
                    // post an empty operation to make sure that 
                    // onDispatcherInactive will be called again
                    idleOperation = dispatcher.BeginInvoke(
                        idleAction,
                        DispatcherPriority.ApplicationIdle);
                };

                // timeout plumbing
                Func<uint> getTimeout;
                if (Timeout.Infinite == millisecondsTimeout)
                    getTimeout = () => INFINITE;
                else
                    getTimeout = () => (uint)Math.Max(0, millisecondsTimeout + startTick - Environment.TickCount);

                DispatcherHookEventHandler onOperationPosted = (s, e) =>
                {
                    // this may occur on a random thread,
                    // trigger a helper event and 
                    // unblock MsgWaitForMultipleObjectsEx inside onDispatcherInactive
                    operationPendingMre.Set();
                };

                DispatcherHookEventHandler onOperationCompleted = (s, e) =>
                {
                    // this should be fired on the Dispather thread
                    Debug.Assert(Thread.CurrentThread == dispatcher.Thread);

                    // do an instant handle check
                    var nativeResult = WaitForSingleObject(handles[0], 0);
                    if (nativeResult == WAIT_OBJECT_0)
                        result = true;
                    else if (nativeResult == WAIT_ABANDONED_0)
                        throw new AbandonedMutexException(-1, handle);
                    else if (getTimeout() == 0)
                        result = false;
                    else if (nativeResult == WAIT_TIMEOUT)
                        return;
                    else
                        throw new InvalidOperationException("WaitForSingleObject");

                    // end the nested Dispatcher loop
                    frame.Continue = false;
                };

                EventHandler onDispatcherInactive = (s, e) =>
                {
                    operationPendingMre.Reset();

                    // wait for the handle or a message
                    var timeout = getTimeout();

                    var nativeResult = MsgWaitForMultipleObjectsEx(
                         (uint)handles.Length, handles,
                         timeout,
                         QS_EVENTMASK,
                         MWMO_INPUTAVAILABLE);

                    if (nativeResult == WAIT_OBJECT_0)
                        // handle signalled
                        result = true;
                    else if (nativeResult == WAIT_TIMEOUT)
                        // timed out
                        result = false;
                    else if (nativeResult == WAIT_ABANDONED_0)
                        // abandonded mutex
                        throw new AbandonedMutexException(-1, handle);
                    else if (nativeResult == WAIT_OBJECT_0 + 1)
                        // operation posted from another thread, yield to the frame loop
                        return;
                    else if (nativeResult == WAIT_OBJECT_0 + 2)
                    {
                        // a Windows message 
                        if (getTimeout() > 0)
                        {
                            // message pending, yield to the frame loop
                            enqueIdleOperation(); 
                            return;
                        }

                        // timed out
                        result = false;
                    }
                    else
                        // unknown result
                        throw new InvalidOperationException("MsgWaitForMultipleObjectsEx");

                    // end the nested Dispatcher loop
                    frame.Continue = false;
                };

                dispatcher.Hooks.OperationCompleted += onOperationCompleted;
                dispatcher.Hooks.OperationPosted += onOperationPosted;
                dispatcher.Hooks.DispatcherInactive += onDispatcherInactive;

                try
                {
                    // onDispatcherInactive will be called on the new frame,
                    // as soon as Dispatcher becomes idle
                    enqueIdleOperation();
                    Dispatcher.PushFrame(frame);
                }
                finally
                {
                    if (idleOperation != null)
                        idleOperation.Abort();
                    dispatcher.Hooks.OperationCompleted -= onOperationCompleted;
                    dispatcher.Hooks.OperationPosted -= onOperationPosted;
                    dispatcher.Hooks.DispatcherInactive -= onDispatcherInactive;
                }

                return result;
            }
        }

        const uint QS_EVENTMASK = 0x1FF;
        const uint MWMO_INPUTAVAILABLE = 0x4;
        const uint WAIT_TIMEOUT = 0x102;
        const uint WAIT_OBJECT_0 = 0;
        const uint WAIT_ABANDONED_0 = 0x80;
        const uint INFINITE = 0xFFFFFFFF;

        [DllImport("user32.dll", SetLastError = true)]
        static extern uint MsgWaitForMultipleObjectsEx(
            uint nCount, IntPtr[] pHandles,
            uint dwMilliseconds, uint dwWakeMask, uint dwFlags);

        [DllImport("kernel32.dll")]
        static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
    }
    #endregion
}

This code hasn't been heavily tested and may contain bugs, but I think I've got the concept right, as far as the question goes.



回答2:

I've had to do similar things before for testing UI's in-proc with UI Automation. The implementation is something like this

public static bool WaitOneAndPump(WaitHandle handle, int timeoutMillis)
{
     bool gotHandle = false;
     Stopwatch stopwatch = Stopwatch.StartNew();
     while(!(gotHandle = waitHandle.WaitOne(0)) && stopwatch.ElapsedMilliseconds < timeoutMillis)
     {
         DispatcherFrame frame = new DispatcherFrame();
         Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
             new DispatcherOperationCallback(ExitFrame), frame);
         Dispatcher.PushFrame(frame);
     }

     return gotHandle;
}

private static object ExitFrame(object f)
{
    ((DispatcherFrame)f).Continue = false;
    return null;
}

I've run into issues scheduling at lower than Background priority before. The issue is, I believe, that WPF hit testing occurs at a higher priority so depending on where the mouse is the ApplicationIdle priority may never get run.

Update

So it seems the above method will peg the CPU. Here's an alternative that uses a DispatcherTimer to check while the method pumps for messages.

public static bool WaitOneAndPump2(this WaitHandle waitHandle, int timeoutMillis)
{
    if (waitHandle.WaitOne(0))
        return true;

    DispatcherTimer timer = new DispatcherTimer(DispatcherPriority.Background) 
    { 
        Interval = TimeSpan.FromMilliseconds(50) 
    };

    DispatcherFrame frame = new DispatcherFrame();
    Stopwatch stopwatch = Stopwatch.StartNew();
    bool gotHandle = false;
    timer.Tick += (o, e) =>
    {
       gotHandle = waitHandle.WaitOne(0);
       if (gotHandle || stopwatch.ElapsedMilliseconds > timeoutMillis)
       {
           timer.IsEnabled = false;
           frame.Continue = false;
       }
    };
    timer.IsEnabled = true;
    Dispatcher.PushFrame(frame);
    return gotHandle;
}