【问题标题】:Update UI thread in winforms application reliably可靠地更新 winforms 应用程序中的 UI 线程
【发布时间】:2016-06-17 10:43:05
【问题描述】:

我有一个长时间运行的机器学习程序,我在后台在 winforms 应用程序中使用所有内核并行运行。我会定期更新 UI 以报告进度。

机器似乎选择了相当随机的时间在 UI 线程上执行消息泵。有时我会在几分钟内没有收到更新,有时我每次发送一条消息都会收到一条消息。

我已经尝试了各种方法来使这个可靠,包括从后台线程进行标准调用,使用来自后台工作人员的进度报告,使用 UI 线程上的计时器来收集信息并显示它,减少最大线程数可以并行运行,弄乱线程优先级等。我发现可靠获取更新的唯一方法是将控制台添加到 winforms 程序并将进度输出到控制台。出于某种原因,这是 100% 可靠的,但它是一个真正的 hack,看起来很乱。

有人知道强制ui线程可靠更新的方法吗?

根据要求:这是复制错误的最基本代码。创建一个带有名为 label1 的标签的表单。该代码尝试每百万次迭代更新一次标签。

Module testmodule
delegate sub invokedelegate(txt as string)

Sub long_running_process()
    Dim x(100000000) As Integer
    Dim cnt As Integer
    Dim syncobject As New Object

    form1.Label1.Text = "started"

    Parallel.ForEach(x, Sub(z)

                            '*** This just put in to make the processors do some work.
                            Dim p As New Random
                            Dim m As Double = p.NextDouble
                            Dim zzz As Double = Math.Cosh(m) + Math.Cos(m)

                            '*** This is the basic updating method.
                            SyncLock syncobject
                                cnt += 1

                                '*** Update every millionth iteration
                                If cnt Mod 1000000 < 1 Then

                                    '**** This is how it is marshalled to the UI thead.
                                    If Form1.InvokeRequired Then
                                        Form1.BeginInvoke(New invokedelegate(AddressOf invokemethod), {cnt})
                                    Else
                                        Form1.Label1.Text = cnt
                                    End If

                                End If
                            End SyncLock
                        End Sub)

    Form1.Label1.Text = "Finished"
End Sub
Sub invokemethod(txt As String)
    form1.Label1.Text = txt
End Sub
end module

【问题讨论】:

  • UI 线程不会选择 时不时地运行消息泵 - 这就是它所做的一切,一遍又一遍,一整天,每一天。如果您没有收到消息,那是因为您没有发送它们 UI 线程太忙而无法及时处理它们。如果您确定要发送消息,请使用分析器来计算您的 UI 线程执行附加到每条消息的处理程序所需的时间。如果它比更新消息之间的时间长,那么您将累积消息积压并且事情将停止工作。 minimal reproducible example 会很有帮助。
  • 另一种可能是资源争用——如果你的线程频繁分配内存,它们可能会独占内存管理器,如果你的 UI 处理程序也需要分配内存,它可能会等待线程提供轮到你了……再一次,没有任何代码来查看你实际在做什么,很难提供具体的建议。
  • 您是否使用BeginInvoke 将您的消息发布回UI 线程消息泵?如果你这样做,我认为你不应该看到任何这样的问题。如果您发布用于编组 UI 更新的代码,这将有助于我们识别问题。
  • 我添加了一个最小的完整且可验证的示例。

标签: .net vb.net multithreading winforms


【解决方案1】:

问题在于Parallel.ForEach 坚持在 UI 线程上运行所有任务。我改变这种情况的一种方法是从BackgroundWorker.DoWork 调用long_running_process,如下所示:

Public Class Form1

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        BackgroundWorker1.RunWorkerAsync()

    End Sub

    Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
        testmodule.long_running_process(Label1)
    End Sub
End Class

请注意,我必须提供 Label1 作为参数,否则 InvokeRequired 总是返回 false。

更改后的long_running_process 如下所示:

Sub long_running_process(lbl As Label)
    Dim x(100000000) As Integer
    Dim cnt As Integer
    Dim syncobject As New Object

    If lbl.InvokeRequired Then
        lbl.BeginInvoke(New MethodInvoker(Sub()
                                              lbl.Text = "started"
                                          End Sub), Nothing)
    End If


    Parallel.ForEach(x, Sub(z)

                            '*** This just put in to make the processors do some work.
                            Dim p As New Random
                            Dim m As Double = p.NextDouble
                            Dim zzz As Double = Math.Cosh(m) + Math.Cos(m)

                            '*** This is the basic updating method.
                            SyncLock syncobject
                                cnt += 1

                                '*** Update every millionth iteration
                                If cnt Mod 1000000 < 1 Then

                                    '**** This is how it is marshalled to the UI thead.
                                    If lbl.InvokeRequired Then
                                        lbl.BeginInvoke(New invokedelegate(AddressOf invokemethod), {cnt.ToString()})
                                    Else
                                        lbl.Text = cnt
                                    End If

                                End If
                            End SyncLock
                        End Sub)

    If lbl.InvokeRequired Then
        lbl.BeginInvoke(New MethodInvoker(Sub()
                                              lbl.Text = "Finished"
                                          End Sub), Nothing)
    End If

End Sub
Sub invokemethod(txt As String)
    Form1.Label1.Text = txt
End Sub

计数器会随着这些变化顺利更新。

【讨论】:

  • 非常感谢。我已经尝试将它作为后台进程运行 - 但不是在示例代码中。它没有奏效。让我感兴趣的是为什么 invokerequired 总是返回 false ,除非您将标签作为参数传递。标签的传递几乎可以肯定是我的问题的解决方案。但是你知道它为什么起作用吗?
  • @GavinPotter VB.NET 项目和编译器增加了魔力,因此您可以通过工厂访问 Form1 作为静态引用,以创建尚不存在的表单。然而,支持集合被标记为[ThreadStatic],这意味着每个线程都有自己的一组活动表单。这意味着 UI 线程上的 Form1 与后台线程上的 Form1 不同。当调用者和表单都在同一个线程上时,InvokeRequired 返回 false。
  • 非常感谢 - 我没有意识到这一点,这导致了我遇到的许多其他问题。
  • VB.NET 和/或项目默认生成的帮助程序类隐藏了在简单场景中可能有用的复杂性。它使诊断潜在错误变得更加困难。我使用 ILSpy 来查看生成的内容以了解正在发生的事情。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-04-19
  • 2010-09-13
  • 1970-01-01
相关资源
最近更新 更多