繁体   English   中英

为什么Parallel.For 会执行WinForms 消息泵,以及如何防止?

[英]Why does Parallel.For execute the WinForms message pump, and how to prevent it?

我正在尝试使用Parallel.For加速一个冗长(几毫秒)的操作*,但是在该方法返回之前,我的 WinForms 应用程序中到处都是 Paint 事件 - 表明它以某种方式触发了消息泵。 但是,整体重绘会导致以不一致的状态访问数据,从而产生不稳定的错误和异常。 我需要确保Parallel.For在阻塞时不会触发 UI 代码。

到目前为止,我对此的研究尚无定论,并粗略地向我指出了诸如同步上下文和TaskScheduler实现之类的东西,但我还没有TaskScheduler这一切。

如果有人可以通过清理一些事情来帮助我,那将不胜感激。

  1. 导致Parallel.For触发 WinForms 消息泵的事件链是什么?
  2. 有什么办法可以完全防止这种情况发生吗?
  3. 或者,有什么方法可以判断 UI 事件处理程序是从常规消息泵调用还是从Parallel.For触发的“忙”消息泵调用?

编辑: * 一些上下文:上述几毫秒操作是游戏引擎循环的一部分,其中 16 毫秒可用于完整更新 - 因此属性“冗长”。 此问题的上下文是在其编辑器中执行游戏引擎核心,这是一个 WinForms 应用程序。 Parallel.For发生在内部引擎更新期间。

这来自 CLR,它实现了永远不允许 STA 线程(又名 UI 线程)阻塞同步对象的约定。 就像 Parallel.For() 一样。 它泵送以确保不会发生死锁。

这会触发 Paint 事件,以及其他一些事件,确切的消息过滤是一个保密的秘密。 它与 DoEvents() 非常相似,但可能导致重入错误的内容被阻止。 就像用户输入一样。

但是很明显你有一个 DoEvents() 风格的错误,重入永远是一个讨厌的错误生成器。 我怀疑您只需要设置一个bool标志以确保 Paint 事件跳过更新,这是最简单的解决方法。 将 Program.cs 中 Main() 方法上的 [STAThread] 属性更改为 [MTAThread] 也是一个简单的修复,但如果您也有正常的 UI,则风险很大。 支持private bool ReadyToPaint; 方法,这是最简单的推理。

但是,您应该调查 Winforms 认为需要 Paint 的确切原因,因为您可以控制游戏循环中的 Invalidate() 调用,所以不应该这样做。 可能会因用户交互触发,例如最小/最大/恢复窗口,但这应该很少见。 地板垫下隐藏着另一个错误的非零几率。

正如已经解释过的, Parallel.For本身不执行 WinForms 消息泵,但由必要的线程同步原语调用的Wait的 CLR 实现导致了该行为。

幸运的是,可以通过安装自定义SynhronizationContext来覆盖该实现,因为所有 CLR 等待实际上都调用当前(即与当前线程关联)同步上下文的Wait方法。

这个想法是调用没有这种副作用的WaitForMultipleObjectsEx API。 我不能说它是否安全,CLR设计人员有他们的理由,但从另一方面来说,他们必须处理许多可能不适用于您的情况的不同场景,因此至少值得一试。

这是课程:

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security;
using System.Threading;
using System.Windows.Forms;

class CustomSynchronizationContext : SynchronizationContext
{
    public static void Install()
    {
        var currentContext = Current;
        if (currentContext is CustomSynchronizationContext) return;
        WindowsFormsSynchronizationContext.AutoInstall = false;
        SetSynchronizationContext(new CustomSynchronizationContext(currentContext));
    }

    public static void Uninstall()
    {
        var currentContext = Current as CustomSynchronizationContext;
        if (currentContext == null) return;
        SetSynchronizationContext(currentContext.baseContext);
    }

    private WindowsFormsSynchronizationContext baseContext;

    private CustomSynchronizationContext(SynchronizationContext currentContext)
    {
        baseContext = currentContext as WindowsFormsSynchronizationContext  ?? new WindowsFormsSynchronizationContext();
        SetWaitNotificationRequired();
    }

    public override SynchronizationContext CreateCopy() { return this; }
    public override void Post(SendOrPostCallback d, object state) { baseContext.Post(d, state); }
    public override void Send(SendOrPostCallback d, object state) { baseContext.Send(d, state); }
    public override void OperationStarted() { baseContext.OperationStarted(); }
    public override void OperationCompleted() { baseContext.OperationCompleted(); }

    public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout)
    {
        int result = WaitForMultipleObjectsEx(waitHandles.Length, waitHandles, waitAll, millisecondsTimeout, false);
        if (result == -1) throw new Win32Exception();
        return result;
    }

    [SuppressUnmanagedCodeSecurity]
    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern int WaitForMultipleObjectsEx(int nCount, IntPtr[] pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable);
}

为了激活它,只需在Application.Run(...)调用之前添加以下行:

CustomSynchronizationContext.Install();

汉斯解释了。那你该怎么办? 最简单的方法是拥有一个volatile bool标志,表明数据是否一致并且可以使用它们进行绘制。

更好但更复杂的解决方案是将Parallel.For替换为您自己的ThreadPool ,并将简单的任务发送到池中。 然后主 GUI 线程将保持对用户输入的响应。

此外,那些简单的任务不能直接更改 GUI,而只能操作数据。 游戏 GUI 必须仅在OnPaint更改。

汉斯解释道。 那你该怎么办? 不要在 UI 线程上运行该循环。 在后台线程上运行它,例如:

await Task.Run(() => Parallel.For(...));

在 UI 线程上阻塞通常不是一个好主意。 不确定这与游戏引擎循环设计的相关性,但这解决了重入问题。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM