[英]How to execute Parallel.For Loop and Parallel.ForEach loop in sequence
[英]Why does Parallel.For execute the WinForms message pump, and how to prevent it?
我正在尝试使用Parallel.For
加速一个冗长(几毫秒)的操作*,但是在该方法返回之前,我的 WinForms 应用程序中到处都是 Paint 事件 - 表明它以某种方式触发了消息泵。 但是,整体重绘会导致以不一致的状态访问数据,从而产生不稳定的错误和异常。 我需要确保Parallel.For
在阻塞时不会触发 UI 代码。
到目前为止,我对此的研究尚无定论,并粗略地向我指出了诸如同步上下文和TaskScheduler
实现之类的东西,但我还没有TaskScheduler
这一切。
如果有人可以通过清理一些事情来帮助我,那将不胜感激。
Parallel.For
触发 WinForms 消息泵的事件链是什么?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.