【问题标题】:Multithreading on Windows FormsWindows 窗体上的多线程
【发布时间】:2009-10-13 01:19:35
【问题描述】:

我想并行化构建在 Windows 窗体之上的 3D 体素编辑器,它使用光线投射器进行渲染,因此划分屏幕并让池中的每个线程来渲染它的一部分应该是微不足道的。

问题出现在 Windows 窗体的线程必须作为 STA 运行 - 我可以让其他线程启动并完成工作,但在等待它们完成时阻塞主线程会导致奇怪的随机死锁。

保持主线程畅通也是一个问题 - 例如,如果用户使用填充工具,输入将在渲染过程中进行处理,这将导致“中间”图像(部分着色的对象,例如例子)。在每一帧之前复制整个图像也是不可行的,因为如果必须每帧都复制,体积足够大,可以抵消任何性能提升。

我想知道是否有任何解决方法可以让 amin 线程在用户看来被阻塞,这样它实际上不会被阻塞,但会将输入处理延迟到下一帧。

如果不可能,是否有更好的设计来处理这个问题?

编辑:阅读 anwsers 我想我不清楚 raycaster 是否实时运行,因此显示进度对话框根本不起作用。不幸的是,FPS 足够低(5-40 取决于各种因素),帧之间的输入会产生不需要的结果。

我已经尝试实现它阻塞 UI 线程并使用 ThreadPool 的一些线程进行处理,除了 STA 的这个问题之外它工作正常。

【问题讨论】:

  • 随机输入事件。单击任务栏上的最小化应用程序通常会执行此操作 - 有时 Windows 会发出哔声(错误哔声),有时会崩溃。

标签: c# winforms multithreading


【解决方案1】:

这是一个常见问题。对于 Windows 窗体,您只能拥有一个 UI 线程。不要在 UI 线程上运行您的算法,因为这样 UI 将显示为冻结状态。

我建议在更新 UI 之前运行您的算法并等待它完成。预先构建了一个名为 BackgroundWorker 的类来执行此操作。

编辑:

关于 UI 线程的另一个事实是它处理所有的鼠标和键盘事件,以及发送到窗口的系统消息。 (Winforms 实际上只是被一个很好的 API 包围的 Win32。)如果 UI 线程饱和,您将无法拥有稳定的应用程序。

另一方面,如果你启动其他几个线程并尝试用它们直接在屏幕上绘制,你可能会遇到两个问题:

  1. 您不应该使用除 UI 线程之外的任何线程在 UI 上绘图。 Windows 控件不是线程安全的。
  2. 如果您有很多线程,它们之间的上下文切换可能会影响您的性能。

请注意,您(和我)不应该声称存在性能问题直到它被测量。您可以尝试在内存中绘制一个帧并在适当的时间将其换入。它被称为double-buffering,在Win32绘图代码中很常见,以避免屏幕闪烁。

老实说,我不知道这对于您的目标帧速率是否可行,或者您是否应该考虑使用更以图形为中心的库,例如 OpenGL。

【讨论】:

  • 我想我不是很清楚,目的是让 UI 在帧被绘制之前显示为冻结状态(5-40 FPS,取决于各种参数)。如果它不断更新,那么图像可能会在帧之间发生部分变化,看起来会很奇怪。
【解决方案2】:

我是否遗漏了什么,或者您是否可以在渲染帧时将渲染控件(以及任何其他生成输入事件的控件)设置为禁用?这将防止不必要的输入。

如果您仍想在渲染时接受事件,但不想在下一帧之前应用它们,您应该启用控件并将事件的详细信息发布到输入队列。然后应该在每一帧的开始处理该队列。

这会影响用户仍然可以单击按钮并与 UI 交互(GUI 线程不会阻塞),并且这些事件在下一帧开始之前对渲染器不可见。在 5 FPS 时,用户应该会看到他们的事件在最坏情况下(2 帧)的 400 毫秒内得到处理,这还不够快,但比线程死锁要好。

大概是这样的:

Public InputQueue<InputEvent> = new Queue<InputEvent>();

// An input event handler.
private void btnDoSomething_Click(object sender, EventArgs e)
{ 
    lock(InputQueue)
    {
         InputQueue.Enqueue(new DoSomethingInputEvent());
    }
}

// Your render method (executing in a background thread). 
private void RenderNextFrame()
{
    Queue<InputEvent> inputEvents = new Queue<InputEvent>();

    lock(InputQueue)
    {
         inputEvents.Enqueue(InputQueue.Dequeue());
    }

    // Process your input events from the local inputEvents queue.
    ....

    // Now do your render based on those events.
    ....
}

哦,在后台线程上进行渲染。你的 UI 线程很宝贵,它应该只做最琐碎的工作。 Matt Brundell 对 BackgroundWorker 的建议有很多优点。如果它不能满足您的要求,那么 ThreadPool 也很有用(而且更简单)。更强大(和复杂)的替代方案是 CCRTask Parallel Library

【讨论】:

  • 这似乎是个好主意,我会试试看它是否有效。
【解决方案3】:

使用ShowDialog 显示模式“请稍候”对话框,然后在渲染完成后关闭它。

这将阻止用户与表单交互,同时仍允许您调用 UI 线程(这可能是您的问题)。

【讨论】:

  • 其实是 ShowDialog,不是 ShowModal ;)
  • 在子窗体关闭之前,ShowDialog 不会停止父窗体的所有处理吗?
  • 不幸的是它是一个实时渲染器,所以我不能这样做。
【解决方案4】:

如果您不想要 BackgroundWorker 提供的所有功能,您可以简单地使用 ThreadPool.QueueUserWorkItem 向线程池添加一些内容并使用后台线程。在后台线程执行其操作时显示某种进度很容易,因为您可以提供委托回调以在特定后台线程完成时通知您。看看ThreadPool.QueueUserWorkItem Method (WaitCallback, Object) 看看我指的是什么。如果你需要更复杂的东西,你总是可以使用 APM 异步方法来执行你的操作。

无论如何,我希望这会有所帮助。

编辑:

  1. 以某种方式通知用户正在对 UI 进行更改。
  2. 在一个(许多)后台线程上使用 ThreadPool 执行您需要对 UI 执行的操作。
  3. 为每个操作保留对操作状态的引用,以便您知道它何时在 WaitCallback 中完成。也许将它们放在某种类型的哈希/集合中以保留对它们的引用。
  4. 每当操作完成时,将其从包含已执行操作的引用的集合中删除。
  5. 一旦所有操作(散列/集合)完成后,其中没有更多引用,就会呈现应用了更改的 UI。或者可能会逐步更新 UI

我在想,如果您在执行操作时对 UI 进行了如此多的更新,这就是导致问题的原因。这也是我推荐使用 SuspendLayout、PerformLayout 的原因,因为您可能已经对 UI 执行了如此多的更新,导致主线程不堪重负。

虽然我不是线程方面的专家,但我只是想通过自己来思考。希望这会有所帮助。

【讨论】:

  • 我已经尝试使用 ThreadPool 但我无法保持循环运行并显示进度条,因为它是一个实时渲染器(速度不够快,无法避免图像在帧)。如果我在等待渲染完成时阻塞 UI 线程,死锁会随机发生。
  • 您可以使用 APM 报告每个项目的进度。我认为您可能会遇到报告有意义的确定性进展。我认为您不想在 UI 线程上执行任何阻塞来完成此操作。当您更新 UI 以反映更改时,您是否在各自的控件上执行 SuspendLayout 和 ResumeLayout?在我看来,您似乎希望在完成更改之前完全暂停布局,但我不确定我是否完全理解您的问题。
【解决方案5】:

在每一帧之前复制整个图像也是不可行的,因为如果必须每帧都复制,体积足够大,可以抵消任何性能提升。

那就不要在每一帧都复制离屏缓冲区。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-30
    • 2015-03-17
    • 2014-04-02
    • 1970-01-01
    • 1970-01-01
    • 2013-06-19
    相关资源
    最近更新 更多