如果前面没有 DoEvents(),为什么 Thread.Join() 会挂起?

Why does Thread.Join() hang if not preceded by DoEvents()?

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

问题是这样的:在FormClosing()中,主线程暂停设备然后调用Join()等待MonitorThread终止,但是Join()从来没有returns ,这会导致应用程序挂起。永远不会到达 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

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

记住:您可能需要很多 Backgroundworker,您可以在它们的 RunWorkerCompleted 事件中附加一种召回顺序。

我相信这是您日常生活中最安全、最好的选择。 CloseDevice 必须放在 FormClosing 事件中,在您设置 BackgroundWorker 的 CANCELATION 之后(使用 BG.cancelAsync)

更新:

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

在我原来的代码中Join()是由主线程调用的,其中"owns"是UI,而MonitorThread调用Invoke()来更新UI .当 MonitorThread 调用 Invoke() 时,它实际上是在 UI 消息队列上安排 UpdateGUI() 的延迟执行,然后阻塞直到 UpdateGUI() 完成。 DeviceRead()UpdateGUI() 共享一个数据缓冲区以提高效率。由于我不清楚的原因,只要主线程在 Join() 中,MonitorThread 就会被阻塞——即使它可能被 DeviceRead() 阻塞,因此不会在 Invoke() 中等待。很明显,这会导致死锁,因为 MonitorThread 无法 运行(因此终止),因此主线程永远不会 returns from 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() returns 在前一个延迟 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