如何在提供WPF Dispatcher事件时等待WaitHandle?

时间:2014-02-08 05:32:42

标签: c# .net wpf multithreading wait

有人给我发了电子邮件,询问我是否有WPF的WaitOneAndPump版本。目标是等待句柄(类似于WaitHandle.WaitOne)并在等待的同时在相同的堆栈帧上抽取WPF Dispatcher事件。

我真的不认为这样的API应该用于任何生产代码,无论是WinForms还是WPF (可能除了UI自动化之外)。 WPF没有公开WinForms'DoEvents的显式版本,这是一个非常好的设计决策,给定the fair share of abuse the DoEvents API has been taking

尽管如此,这个问题本身很有意思,所以我将把它作为一个练习并发布任何我想出的答案。如果感兴趣,请随意发布您自己的版本。

2 个答案:

答案 0 :(得分:6)

版本WaitOneAndPump我提出了使用DispatcherHooks EventsMsgWaitForMultipleObjectsEx,以避免运行busy-waiting loop

同样,在生产代码中使用此WaitOneAndPump(或任何其他嵌套的消息循环变体)几乎总是一个糟糕的设计决策。我只能想到两个.NET合法使用嵌套消息循环的API:Window.ShowDialogForm.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
}

此代码尚未经过严格测试,可能包含错误,但我认为我的概念是正确的,就问题而言。

答案 1 :(得分:2)

我之前必须做类似的事情,用UI自动化测试UI的进程。实现是这样的

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;
}

我之前遇到过低于后台优先级的问题。我相信,问题是WPF命中测试的优先级更高,因此根据鼠标的位置,ApplicationIdle优先级可能永远不会运行。

<强>更新

所以看起来上面的方法会挂掉CPU。这是一种替代方法,在方法为消息提取时使用DispatcherTimer进行检查。

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;
}