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

时间:2015-09-17 18:54:25

标签: .net vb.net multithreading winforms formclosing

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

问题是:在FormClosing()中,主线程暂停设备,然后调用Join()等待MonitorThread终止,但Join()永远不会返回,这会导致应用程序挂。从未到达MonitorThread末尾的断点,表明MonitorThread在某种程度上被饿死了。但是,如果我在DoEvents()之前插入Join(),那么一切都按预期工作。为什么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

2 个答案:

答案 0 :(得分:0)

更新:

我已经提出了几个不依赖于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()在前一个延迟UpdateGUI()完成之前返回,则共享的devdata缓冲区将被过早覆盖。解决方法是为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 的每次调用创建设备数据的唯一副本,并将其作为参数传递。

xmltodict

答案 1 :(得分:-2)

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

请记住:您可能需要许多背景工作者,并且可以在其RunWorkerCompleted事件中附加一种召回顺序。

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