I have a class that raises an event after a specified time (it uses a System.Timers.Timer
inside). In my test code, I created a Stopwatch
which I started before the class was created and set the callback for the event to stop that Stopwatch
. Then, I blocked until Not Stopwatch.IsRunning
. Simple, right?
My original blocking code was
While Stopwatch.IsRunning
End While
but I found that having an empty while loop like that never allowed my callback to fire! As soon as I put debugging code into the while loop, it worked!:
Dim lastSecond As Integer = 0
While sw.IsRunning
If (Date.Now.Second > lastSecond) Then
lastSecond = Date.Now.Second
Console.WriteLine("blocking...")
End If
End While
What causes this strange behavior, and more importantly, what's the simplest code I can put into my blocking section that will allow the event to fire?
While Stopwatch.IsRunning
End While
It is one of the Great Sins in threading, called a "hot wait loop". Threading has many sins, and many of them have no yellow tape at all, but this one is particularly insidious. The principal problem is that you keep one processor core burning red hot, testing the IsRunning property in a tight loop.
This begets a very nasty problem when you use the x86 jitter, it generates code in the release build that reads the IsRunning property backing field variable in a cpu register. And tests the cpu register value over and over again, without reloading the value from the field. That's the ultimate deadlock, it can never exit the loop. You bumped it out of that mode by editing the code or by using the debugger. To avoid it, the backing field of the property must be declared volatile but that's not something you can do in VB.NET, nor is it the proper fix.
Instead you should use a proper synchronization object, one that lets you signal another thread that something happened. A good one is the AutoResetEvent, you'd use it like this:
Dim IsCompleted As New AutoResetEvent(False)
Private Sub WaitForTimer()
IsCompleted.WaitOne()
''etc..
End Sub
Private Sub timer_Elapsed(ByVal sender As Object, ByVal e As EventArgs) Handles timer.Elapsed
IsCompleted.Set()
timer.Stop()
End Sub
Beware that AutoResetEvent has yellow tape missing as well. Calling Set() more than once while the other thread hasn't yet called WaitOne() ends up poorly.