【问题标题】:Why does Thread.Join() hang if not preceded by DoEvents()?如果前面没有 DoEvents(),为什么 Thread.Join() 会挂起?
【发布时间】:2015-09-17 18:54:24
【问题描述】:

我有一个创建设备监控线程的 VB.NET 应用程序。 MonitorThread 是一个“无限”循环,它通过阻塞函数DeviceRead() 等待设备数据,然后用数据更新表单控件。当设备停止时,DeviceRead() 返回零,这会导致 MonitorThread 终止。这一切都很完美。

问题是这样的:在FormClosing()中,主线程暂停设备,然后调用Join()等待MonitorThread终止,但Join()永远不会返回,导致应用程序挂起。永远不会到达 MonitorThread 末尾的断点,这表明 MonitorThread 不知何故被饿死了。但是,如果我在Join() 之前插入DoEvents(),那么一切都会按预期工作。为什么需要DoEvents() 来防止挂起,有没有更好的方法来做到这一点?

我的代码的简化版:

Private devdata As DEVDATASTRUCT = New DEVDATASTRUCT
Private MonitorThread As Threading.Thread = New Threading.Thread(AddressOf MonitorThreadFunction)

Private Sub FormLoad(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
  DeviceOpen()           ' Open the device and start it running.
  MonitorThread.Start()  ' Start MonitorThread running.
End Sub

Private Sub FormClosing(ByVal sender As Object, ByVal e As FormClosingEventArgs) Handles MyBase.Closing
  DeviceHalt()           ' Halt device. Subsequent DeviceRead() calls will return zero.
  Application.DoEvents() ' WHY IS THIS NECESSARY? IF OMITTED, THE NEXT STATEMENT HANGS.
  MonitorThread.Join()   ' Wait for MonitorThread to terminate.
  DeviceClose()          ' MonitorThread completed, so device can be safely closed.
End Sub

Private Sub MonitorThreadFunction()
  While (DeviceRead(devdata))   ' Wait for device data or halted (0). Exit loop if halted.
    Me.Invoke(New MethodInvoker(AddressOf UpdateGUI))  ' Launch GUI update function and wait for it to complete.
  End While
End Sub

Private Sub UpdateGUI()
  ' copy devdata to form controls
End Sub

【问题讨论】:

  • 第一个问题做得很好。
  • 在 doevents 之前,您是否检查过哪个线路监视器线程实际位于?
  • 使用 Invoke() 很危险,很容易出现死锁。当你调用 Join() 时当然会这样做。 Invoke() 无法完成,因为您的 UI 线程卡在 Join 调用中。由于线程卡在 Invoke 调用中,Join 无法完成。死锁城。 DoEvents() 隐藏了问题,它没有解决它,因为时间很关键。仅在必要时使用 Invoke()。你没有,你没有使用它的返回值。所以你可以使用 BeginInvoke() 代替。
  • @Thraka,我不知道如何确定 MonitorThread 位于哪一行,但我认为它在 doevents 之前在 DeviceRead() 中等待,因为没有可用的设备数据(设备已停止)。也就是说,DeviceRead() 应该在 DeviceHalt() 执行后不久返回零,因为 DeviceHalt() 取消了等待。
  • 这不可能,UI 线程一次不能执行多个方法。调用队列将调用序列化。而UpdateGUI与死锁无关。只需这样做the right way

标签: .net vb.net multithreading winforms formclosing


【解决方案1】:

更新:

我想出了几个不依赖 DoEvents 的解决方案来解决 Join() 的挂起问题。

在我的原始代码中,Join() 由“拥有” UI 的主线程调用,MonitorThread 调用Invoke() 来更新 UI。当 MonitorThread 调用 Invoke() 时,它实际上是在 UI 消息队列上调度 UpdateGUI() 的延迟执行,然后阻塞直到 UpdateGUI() 完成。 DeviceRead()UpdateGUI() 共享一个数据缓冲区以提高效率。由于我不清楚的原因,只要主线程在Join() 中,MonitorThread 就会被阻塞——即使它可能被DeviceRead() 阻塞,因此不会在Invoke() 中等待。很明显,这会导致死锁,因为 MonitorThread 无法运行(并因此终止),因此主线程永远不会从 Join() 返回。

解决方案 1:

避免从主线程调用Join()。在FormClosing() 中,主线程启动 TerminatorThread 并取消表单关闭。由于主线程没有被Join()阻塞,MonitorThread能够完成。同时,TerminatorThread 在Join() 中等待,直到 MonitorThread 完成,然后关闭设备并终止应用程序。

Private devdata As DEVDATASTRUCT = New DEVDATASTRUCT  ' shared data buffer

Private Sub FormClosing(ByVal sender As Object, ByVal e As FormClosingEventArgs) Handles MyBase.Closing
  Dim t As Threading.Thread = New Threading.Thread(AddressOf TerminatorThread)
  e.Cancel = True        ' Cancel the app close.
  DeviceHalt()           ' Halt device.
  t.Start()              ' Launch TerminatorThread.
End Sub

Private Sub TerminatorThread()
  MonitorThread.Join()   ' Wait for MonitorThread to terminate.
  DeviceClose()          ' MonitorThread completed, so device can be safely closed.
  Application.Exit()     ' Close app.
End Sub

Private Sub MonitorThreadFunction()
  While (DeviceRead(devdata))   ' Wait for device data or device halted (0).
    Me.Invoke(New MethodInvoker(AddressOf UpdateGUI))  ' Launch UpdateGUI() and wait for it to complete.
  End While
End Sub

Private Sub UpdateGUI()
  ' copy the shared devdata buffer to form controls
End Sub

解决方案 2:

避免在监视器线程中等待UpdateGUI() 完成。这是通过调用BeginInvoke() 而不是Invoke() 来完成的,它仍然安排延迟执行UpdateGUI(),但不会等待它完成。但是,BeginInvoke() 有一个不幸的副作用:它可能导致数据丢失,因为如果 DeviceRead() 在之前的延迟 UpdateGUI() 完成之前返回,共享的 devdata 缓冲区将被过早覆盖。解决方法是为每次调用 UpdateGUI() 创建设备数据的唯一副本,并将其作为参数传递。

Private Delegate Sub GUIInvoker(ByVal devdata As DEVDATASTRUCT)

Private Sub FormClosing(ByVal sender As Object, ByVal e As FormClosingEventArgs) Handles MyBase.Closing
  DeviceHalt()           ' Halt device.
  MonitorThread.Join()   ' Wait for MonitorThread to terminate.
  DeviceClose()          ' MonitorThread completed, so device can be safely closed.
End Sub

Private Sub MonitorThreadFunction()
  Dim devdata As DEVDATASTRUCT = New DEVDATASTRUCT  ' private buffer
  While (DeviceRead(devdata))   ' Wait for device data or device halted (0).
    Me.BeginInvoke(New GUIInvoker(AddressOf UpdateGUI), devdata)  ' Launch UpdateGUI() and return immediately.
  End While
End Sub

Private Sub UpdateGUI(ByVal devdata As DEVDATASTRUCT)
  ' copy the unique devdata to form controls
End Sub

【讨论】:

    【解决方案2】:

    我认为您应该使用 BackgroundWorker,因为它与主线程一起终止。

    请记住:您可能需要许多 Backgroundworkers,您可以在它们的 RunWorkerCompleted 事件中附加一种召回命令。

    我相信这是您日常工作的更安全和最佳选择。并且 CloseDevice 必须放在 FormClosing 事件中,在你设置 BackgroundWorker 的 CANCELATION 之后(使用 BG.cancelAsync)

    【讨论】:

    • @Davis BS,感谢您的回复,但这似乎并没有解决我的问题:找到一种干净的方法来在应用程序关闭时停止和关闭设备。
    • 我猜问题的重点是终止应用程序而不挂起它。考虑到您不会停止设备,您只会停下来阅读它。使用 BackgroundThread(甚至使用 ThreadPool),您可以取消例程而不会出现任何与此相关的问题 - 但当然,如果设备必须停止,问题不是由于线程,而是取消它的确切时间。换句话说,您必须有一个布尔值来指示设备不是“正在读取/访问”才能正常停止它(我猜)。
    • 我相信避免挂起所必需的 DoEvents 是由于应用程序循环整个线程池,在这种情况下,MonitorThread 完成了它的循环,并且您可以暂停设备。跨度>
    • 您还可以使用 PUBLIC/FRIEND 变量来监视线程操作,以指示线程何时工作。
    猜你喜欢
    • 2016-03-28
    • 1970-01-01
    • 1970-01-01
    • 2021-06-09
    • 2012-02-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多