如何在处理WPF Dispatcher事件时等待WaitHandle?

5
有人给我发邮件,问我是否有适用于WPF的WaitOneAndPump版本。目标是等待一个句柄(类似于WaitHandle.WaitOne),并在等待期间使用WPF Dispatcher事件进行泵送,位于同一堆栈帧上。
我真的不认为这样的API应该用于任何生产代码中,无论是WinForms还是WPF(也许除了UI自动化)。 WPF没有公开WinForms的DoEvents的显式版本,这是一个非常好的设计决策,考虑到DoEvents API所经历的滥用
尽管如此,问题本身很有趣,因此我将把它作为一项练习,并发布我可能想出的答案。如果感兴趣,可以随时发布您自己的版本。

不禁想知道,这个有什么用例? - Anton Tykhyy
@AntonTykhyy,到目前为止,我还没有在生产代码中使用过这个,尽管我认为它可能可用于UI自动化、单元测试,以及也许这种情况 - noseratio - open to work
我仍然不明白为什么需要在同一堆栈框架上等待和泵送。在您引用的情况下,接受的答案比您在此提问中进行的肮脏黑客行为要好得多。您正在依赖于WPF实现细节,包括未记录的细节。这只是在应用程序代码或单元测试中寻求麻烦,并且如果可能,我会避免使用它。 - Anton Tykhyy
关于WPF如何处理消息的任何内容。 - Anton Tykhyy
1
糟糕,我忘记了有些可等待对象(互斥锁)需要关注等待它们的线程。抱歉。 - Anton Tykhyy
显示剩余4条评论
2个回答

7
我提供的WaitOneAndPump版本使用DispatcherHooks事件MsgWaitForMultipleObjectsEx,以避免运行繁忙等待循环
但是需要注意,在生产代码中使用此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
}

这段代码没有经过严格测试,可能存在错误,但根据问题描述,我认为我已经掌握了正确的概念。


相关:在UI线程同步取消挂起的任务 - noseratio - open to work

3

我以前也曾为使用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;
}

那会触发所有事件,但也会是一个忙等待循环,其中一个 CPU 核心会一直运行在 100%。或者我漏掉了什么?无论如何加1 :) - noseratio - open to work
据我所知,它不会占用CPU,因为大部分时间都花在PushFrame内执行其他UI操作上,但我明天会再次确认。 - Mike Zboray
1
当我在我的四核i5 CPU上尝试时,ProcessExplorer显示约25%,这意味着其中一个内核的负载达到了100%。我使用了此测试。 - noseratio - open to work
1
@Noseratio 你是正确的。请看我的更新,我使用相同的代码进行了测试,不会使CPU占用率飙升。 - Mike Zboray
Mikez,这种方法仍然会轮询句柄(每50毫秒),我更愿意采用基于MsgWaitForMultipleObjectsEx的解决方案,但这肯定看起来更好,而且你的解决方案更具可移植性。谁曾经对此进行了负面评价,应该重新考虑。 - noseratio - open to work

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接