1

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
Lambtron
  • 69
  • 8
  • Good job on your first question. – Thraka Sep 17 '15 at 19:03
  • Have you checked to see which line monitor thread is actually sitting on prior to the doevents? – Thraka Sep 17 '15 at 19:07
  • 3
    Using Invoke() is dangerous, it is very apt to case deadlock. And certainly does when you call Join(). The Invoke() cannot complete because your UI thread is stuck in the Join call. The Join can't complete because the thread is stuck in the Invoke call. Deadlock city. DoEvents() hides the problem, it doesn't solve it since timing is critical. Only use Invoke() when you have to. You don't, you are not using its return value. So you can use BeginInvoke() instead. – Hans Passant Sep 17 '15 at 19:27
  • @Thraka, I don't know how to determine which line MonitorThread is sitting on but I assume it's waiting in DeviceRead() prior to doevents because no device data is available (the device is halted). That said, DeviceRead() should return zero shortly after DeviceHalt() executes because DeviceHalt() cancels the wait. – Lambtron Sep 17 '15 at 19:59
  • @Hans Passant: I can't use BeginInvoke() because it could cause multiple, concurrent instances of UpdateGUI() to be spawned. Also, MonitorThread is never "stuck" in the Invoke call because UpdateGUI() is non-blocking. – Lambtron Sep 17 '15 at 20:00
  • 1
    That's not possible, the UI thread cannot execute more than one method at a time. The invoke queue serializes the invokes. And UpdateGUI has nothing to do with the deadlock. Just do this [the right way](http://stackoverflow.com/a/1732361/17034). – Hans Passant Sep 17 '15 at 20:06
  • @Hans Passant, Would you please clarify? If UpdateGUI has nothing to do with the hypothetical deadlock then why do you say that MonitorThread is stuck in the Invoke call? It seems to me that MonitorThread spends most of its time waiting in DeviceRead() -- a blocking function -- not in the non-blocking UpdateGUI, which only executes every now and then. – Lambtron Sep 17 '15 at 20:16
  • Hmm, It didn't sound "hypothetical" to me. Once red flag is "The main thread halts the device". A good way to get that DeviceRead() to unblock and the Invoke() call to be made just as the main thread is in Join(). That turned the once-a-month undebuggable deadlock into almost-always deadlock, you are very lucky. – Hans Passant Sep 17 '15 at 20:26
  • @Hans: When main thread is in Join(), DeviceRead() returns 0 -- which terminates MonitorThread and bypasses the Invoke() call. Where's the deadlock? BTW, tried to move this to chat but insufficient reputation to do so. – Lambtron Sep 17 '15 at 20:38
  • Post your DeviceHalt and DeviceRead code – Thraka Sep 17 '15 at 20:47
  • @Thraka: DeviceRead() and DeviceHalt() are DLL functions. BTW, I've done virtually the same thing I'm doing here in a C project and had no problems. I've added devdata to the above source to clarify a point: multiple instances of UpdateGUI() cannot be allowed as they would share common devdata -- something that seems to happen when I use BeginInvoke() instead of Invoke(). I don't understand how there can be a deadlock because it seems like Join() should simply block the main thread until MonitorThread completes, and nothing should be blocking MonitorThread at all. – Lambtron Sep 17 '15 at 21:40
  • I'm just wondering if all of your problems would go away if you switched to use the Task api which is much better for multithreading scenarios. I honestly think that you're just hitting a deadlock because the sub-thread is just sitting at invoke and the main thread is sitting at join. `DoEvents` seems to let the invoke process completely and frees the sub-thread for the join to work. With the Task api you can create the sub thread as a task, and then add a continuation task to it which calls `DeviceClose`. So all you have to do in your FormClosing event is call `DeviceHalt`. – Thraka Sep 18 '15 at 16:09

2 Answers2

0

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
Lambtron
  • 69
  • 8
-2

I believe you should utilize BackgroundWorker, since it terminates together with the main Thread.

Remember: you may have many Backgroundworkers you need and you can attach a kind of recall order in their RunWorkerCompleted event.

I believe it´s the safer and best option to your routine. And the CloseDevice must be put in the FormClosing event, AFTER you set CANCELATION of the BackgroundWorker (use BG.cancelAsync)

David BS
  • 1,822
  • 1
  • 19
  • 35
  • @Davis BS, Thanks for the reply, but this doesn't seem to address my problem: finding a clean way to halt and close the device when the app is closing. – Lambtron Sep 17 '15 at 20:06
  • I guess the problem is focused on terminate the application without hang it. Consider you won´t halt the device, you just will stop to read it. With BackgroundThread (or even using the ThreadPool) you can cancel the routine without any problem related to this - but of course, if the device MUST be halted, the problem is not due to the thread but the EXACT time to cancel it. In other words, you must have a boolean to indicate the device is not "on reading/accessing" to halt it normally (I guess). – David BS Sep 17 '15 at 21:20
  • I believe the DoEvents necessary to avoid the hang is due to application cycle the entire POOL of threads, and in this case, you MonitorThread completes its cycle and you have the device free to be halted. – David BS Sep 17 '15 at 21:23
  • You may also monitor the Thread action using a PUBLIC/FRIEND variable to indicate whenever the Thread is working. – David BS Sep 17 '15 at 21:25