为什么 Parallel.For 会执行 WinForms 消息泵,以及如何防止它
本文关键字:何防止 消息 For Parallel 执行 WinForms 为什么 | 更新日期: 2023-09-27 18:35:42
我正在尝试使用 Parallel.For
加速冗长(几毫秒)的操作*,但在该方法返回之前,我的 WinForms 应用程序中到处都是 Paint 事件 - 这表明它以某种方式触发了消息泵。但是,整体重绘会导致访问处于不一致状态的数据,从而产生不稳定的错误和异常。我需要确保Parallel.For
在阻止时不会触发 UI 代码。
到目前为止,我对此的研究尚无定论,并粗略地指出了同步上下文和TaskScheduler
实现之类的东西,但我还没有理解这一切。
如果有人能通过清理一些事情来帮助我,那将不胜感激。
- 导致
Parallel.For
触发 WinForms 消息泵的事件链是什么? - 有什么方法可以完全防止这种情况发生吗?
- 或者,有没有办法判断 UI 事件处理程序是从常规消息泵调用的,还是从
Parallel.For
触发的"忙"消息泵调用的?
编辑:*一些上下文:上述几毫秒的操作是游戏引擎循环的一部分,其中16毫秒可用于完整更新 - 因此属性"冗长"。此问题的上下文是在其编辑器中执行游戏引擎核心,这是一个 WinForms 应用程序。 Parallel.For
在内部引擎更新期间发生。
这来自 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 线程上阻止通常不是一个好主意。不确定这与游戏引擎循环设计有多相关,但这解决了重入问题。