I have a VB.NET app that creates a device monitoring thread. MonitorThread is an "endless" loop that waits for device data via blocking function DeviceRead()
and then updates form controls with the data. When the device is halted, DeviceRead()
returns zero, which causes MonitorThread to terminate. This all works perfectly.
The problem is this: In FormClosing()
, the main thread halts the device and then calls Join()
to wait for MonitorThread to terminate, but Join()
never returns, which causes the app to hang. A breakpoint at the end of MonitorThread is never reached, indicating that MonitorThread is somehow being starved. However, if I insert DoEvents()
before Join()
then everything works as expected. Why should DoEvents()
be necessary to prevent a hang, and is there a better way to do this?
Simplified version of my code:
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
UPDATE:
I've come up with a couple of solutions to the hanging Join()
that don't depend on DoEvents.
In my original code Join()
is called by the main thread, which "owns" the UI, and MonitorThread calls Invoke()
to update the UI. When MonitorThread calls Invoke()
it is actually scheduling deferred execution of UpdateGUI()
on the UI message queue and then blocks until UpdateGUI()
completes. DeviceRead()
and UpdateGUI()
share a single data buffer for efficiency. For reasons that are unclear to me, MonitorThread is blocked whenever the main thread is in Join()
-- even when it is likely blocked by DeviceRead()
and therefore not waiting in Invoke()
. What is clear is that this causes a deadlock because MonitorThread cannot run (and thus terminate), and consequently the main thread never returns from Join()
.
SOLUTION 1:
Avoid calling Join()
from the main thread. In FormClosing()
the main thread launches TerminatorThread and cancels the form close. Since the main thread is not blocked by Join()
, MonitorThread is able to complete. Meanwhile, TerminatorThread waits in Join()
until MonitorThread completes, then closes the device and terminates the app.
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
SOLUTION 2:
Avoid waiting for UpdateGUI()
completion in the monitor thread. This is accomplished by calling BeginInvoke()
instead of Invoke()
, which still schedules deferred execution of UpdateGUI()
but does not wait for it to complete. BeginInvoke()
has an unfortunate side-effect, however: it can result in dropped data because the shared devdata buffer will be overwritten prematurely if DeviceRead()
returns before the previous deferred UpdateGUI()
has completed. The workaround for this is to create a unique copy of device data for each invocation of UpdateGUI()
and pass it as an argument.
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