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

本文关键字:何防止 消息 For Parallel 执行 WinForms 为什么 | 更新日期: 2023-09-27 18:35:42

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

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

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

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

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

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

这来自 CLR,它实现了永远不允许 STA 线程(也称为 UI 线程)在同步对象上阻止的协定。 就像 Parallel.For() 一样。 它泵以确保不会发生死锁。

这会触发 Paint 事件,而其他一些事件,确切的消息过滤是一个保存完好的机密。 它与DoEvents()非常相似,但可能导致重入错误的东西被阻止了。 比如用户输入。

但很明显,你有一个 DoEvents() 风格的错误,重新进入永远是一个令人讨厌的错误生成器。 我怀疑您只需设置一个布尔标志即可确保 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 线程上阻止通常不是一个好主意。不确定这与游戏引擎循环设计有多相关,但这解决了重入问题。