如果前面没有 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
我有一个 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