Sunday, December 2, 2007

NullReferenceException when using a Threading Timer with a dueTime of 0

I found a very interesting behaviour the other day when working with some multithreaded code. Take a look at the following example:


Module Module1

Private Ticker As System.Threading.Timer

Sub Main()
' Construct a timer with a dueTime of
' 0 so that it is called immediately.
Ticker = New System.Threading.Timer( _
AddressOf DoThreadedWorkJustOnce, _
New Object, _
0, _
System.Threading.Timeout.Infinite)
End Sub

Sub DoThreadedWorkJustOnce(ByVal state As Object)
' Reset the Ticker so that it
' doesn't get executed again.
Ticker.Change( _
System.Threading.Timeout.Infinite, _
System.Threading.Timeout.Infinite)
' Do actual work...
End Sub

End Module

What this module does is when it starts execution, it spawns another thread using a System.Threading.Timer object. The other thread is to start work immediately, with a dueTime of 0. Looks harmless enough.

But there's a subtle problem. It's that the Timer object can become instantiated, and the other thread can start executing, before the Timer object can be assigned to the local variable Ticker. What you can end up with in the DoThreadedWorkJustOnce method is a NullReferenceException.

Very interesting!

So what's the solution? Here's one way:



Module Module1

Private Ticker As System.Threading.Timer

Sub Main()
' Construct a timer with a dueTime of
' 0 so that it is called immediately.
Ticker = New System.Threading.Timer( _
AddressOf DoThreadedWorkJustOnce, _
New Object, _
System.Threading.Timeout.Infinite, _
System.Threading.Timeout.Infinite)
Ticker.Change( _
0, _
System.Threading.Timeout.Infinite)

End Sub

Sub DoThreadedWorkJustOnce(ByVal state As Object)
' Reset the Ticker so that it
' doesn't get executed again.
Ticker.Change( _
System.Threading.Timeout.Infinite, _
System.Threading.Timeout.Infinite)
' Do actual work...
End Sub

End Module


I suppose you could have instead changed the declaration line of Ticker to the double-infinite statement. You could have also wrapped the assignment in a synclock and then have another synclock inside the DoThreadedWorkJustOnce method on the same locked object.

No comments: