Introduction
(Thanks to Gadwin PrintScreen for my screenshots. It is an excellent program.)
Cool Blinkies is a thread-safe and stable indicator blinky light group, that allows either multiple blinkies on:
CoolBlinkies1.A_AllowMultipleOn = True
or a single blinky on:
CoolBlinkies1.A_AllowMultipleOn = False
It has variable degrees of "latency":
CoolBlinkies1.A_Latency = uOneBlinky.LightLatencies.Medium
According to this list:
Public Enum LightLatencies
None
[Short]
Medium
[Long]
End Enum
Latency allows the user to see that a blinky light went on, even if it was turned on only for an instant. If you need even more latency, you could look at the uOneBlinky:Private Sub SetLatency.
You set the number of blinky lights in your group:
CoolBlinkies1.A_NumberOfLights = 5
The color of the blinkies:
CoolBlinkies1.A_LightColour = uOneBlinky.LightColours.Red
According to this list:
Public Enum LightColours
Red
Blue
Green
Yellow
End Enum
Make it a row of blinky lights:
CoolBlinkies1.A_Orientation = Orientations.Horizontal
or a column of blinky lights as in the screenshot above.
CoolBlinkies1.A_Orientation = Orientations.Vertical
You set the margin between blinky lights:
CoolBlinkies1.A_Margin = 3
and whether a blinky light should stay on when you turn it on:
CoolBlinkies1.A_MomentaryOn = False
or turn itself off after a period of time ("latency"):
CoolBlinkies1.A_MomentaryOn = True
according to the A_Latency property described above.
If you use the A_MomentaryOn feature, you can also have the "first" or "zero" blinky light as a "default" blinky:
CoolBlinkies1.A_FirstLightIsOnByDefault = True
In this case, the A_MomentaryOn property should be True. Then all you have to do is tell the group which blinky light to turn on:
A_LightState (Index) = uOneBlinky.LightStates.LightOn
That blinky light will then turn itself off following the latency period. And the "default" blinky light will go back on.
This can be used, for example, in a case where the text beside the "default" blinky light is something like "Idle". Normally you want that blinky light on steady, and when you want to show something "non-idle", you just turn the other blinky light on and do nothing more. After the latency period, the other blinky light will turn itself off, and the "default" blinky light will come back on.
Background
The challenge: Multiple threads controlling a row or column of indicator lights.
Seems simple, just turn them on and off, right? But sometimes the blinky light might get turned on and then off so quickly, that the user cannot even tell that the blinky light was ever on at all. So, you want a blinky light to stay on long enough for the human eye to notice it. That means delaying before drawing the blinky light in the "off colour". This is "latency" .
You shouldn't do such "delaying" on the same thread as the request, since that would delay that thread. Rather the blinky should receive the request to turn itself off, and allow another thread to do the delay, rather than blocking the calling thread. This allows the call to return immediately, with no delay.
After trying to do this with threading, I settled on doing it with Threading.Timer. (That is different from Windows.Forms.Timer, by the way.)
I have found the Threading.Timer to be reliable, accurate, and flexible. Each individual blinky light, when asked to turn itself off, uses (and reuses) an instance of Threading.Timer to delay for a length of time corresponding to its "Latency" property before redrawing itself in the "off colour".
Declare the Threading.Timer (and an infinite delay to prevent the timer from auto-repeating):
Private mLatencyTimer As Threading.Timer
Private mNoAutoRepeatInfiniteTimeSpan As TimeSpan _
= New TimeSpan(Threading.Timeout.Infinite)
Set up the Threading.Timer:
Public Sub New( )
...
Dim QuickDelayForInitialSetup As New TimeSpan(100)
mLatencyTimer = New Threading.Timer( _
AddressOf LatencyTimerControl, Nothing, _
QuickDelayForInitialSetup, _
mNoAutoRepeatInfiniteTimeSpan)
End Sub
Here is the sub referenced by the Threading.Timer constructor:
Private Sub LatencyTimerControl()
If mCurrentLightState = LightStates.DelayingTowardsOff _
Or mMomentaryOn = True Then
mCurrentLightState = LightStates.LightOff
Invalidate()
RaiseEvent LatencyDone(mMyPosition)
End If
End Sub
But with multiple threads potentially asking the same blinky light to turn on/off, there is another issue.
When one blinky light in a group turns on, the group must turn off the last blinky light that was on (unless you have set the AllowMultipleOn property to True). You want it to behave like a group of RadioButtons: If one is blinky light is "selected", the last one selected must be de-selected.
But in a case where many threads may be turning different blinky lights within a group, the "group" (the parent control, uCoolBlinkies) must remember the proper order to turn them off again. You can't store such values in a class-level Field, because a reentrant thread might change that value before it could be used to turn off the corresponding blinky light.
After much fiddling around with different ideas, I finally resolved to use a Queue (Of T) to store the indexes of the blinky lights that need to be turned off, and a BackGroundWorker to pull those indexes off the Queue and tell the corresponding blinky light to turn itself off.
Declare the Queue and BackgroundWorker:
Private mTurnOffQueue As New Queue(Of Integer)
Private WithEvents mTurnOffBW As BackgroundWorker
Set up the BackgroundWorker and arrange for its shutdown:
Private Sub uCoolBlinkies_Load _
(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles Me.Load
mTurnOffBW = New BackgroundWorker
mTurnOffBW.WorkerSupportsCancellation = True
mTurnOffBW.RunWorkerAsync()
End Sub
Private Sub uCoolBlinkies_Disposed _
(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles Me.Disposed
mStopBW = True
mTurnOffBW.CancelAsync()
End Sub
And here's how to link it to the sub that actually does the work:
Private Sub mTurnOffBW_DoWork(ByVal sender As Object, _
ByVal e As DoWorkEventArgs) _
Handles mTurnOffBW.DoWork
Dim BW As BackgroundWorker = CType(sender, BackgroundWorker)
CheckForTurnOffs(BW)
End Sub
Private Sub CheckForTurnOffs(ByVal ABW As BackgroundWorker)
Do
If mTurnOffQueue.Count = 0 Then
Thread.Sleep(100)
Else
mLights(mTurnOffQueue.Dequeue).A_LightState = _
uOneBlinky.LightStates.LightOff
End If
Loop Until mStopBW
End Sub
The above sub CheckForTurnOffs works continually, either sleeping if no items are on the Queue, or Dequeueing the index of a light that needs turning off and telling the light to turn itself off.
When a light needs turning on, you tell it to turn on, and then you might have to tell another light to turn off if you are not allowing multiple lights to be on. Below I turn a light on and then turn the last light off, unless it has not yet been defined (Index = -1). To turn the light off, I just pop the index of the light onto the queue, and forget about it. Once it is Queued, I can update the mLastLightOn to the current light I just turned on.
mLights(Index).A_LightState = uOneBlinky.LightStates.LightOn
If Not mAllowMultipleOn Then
If mLastLightOn > -1 Then mTurnOffQueue.Enqueue(mLastLightOn)
mLastLightOn = Index
End If
Using the Code
I suggest running the project. (Make sure the Tester project is the "Startup Project"). Almost all of the properties are demonstrated on the user interface.
As you play with the demo, keep in mind that in your project, the blinky lights won't be controlled with a TrackBar. The TrackBar is there to let you turn blinky lights on and off.
Right now, the Blinky Group (a "row" or "column" of blinky lights) is a UserControl called uCoolBlinkies. It is the parent control. (I suppose I could have made it a Component based on Panel or something, but I didn't yet try that.)
uCoolBlinkies owns a List (Of T) individual uOneBlinky.
uOneBlinky knows how to repaint itself as an "on" light or an "off" light, using red, yellow, blue or green colour. When it's told to turn itself off, it knows how to set its timer to allow latency before repainting in the "off" colour.
uCoolBlinkies remembers which lights have been on and which need turning off, in the correct order.
I have all public properties and methods prefixed with "A_" so they go to the top of the intellisense list, and are not mixed in with other items. Anyone have a better way of grouping custom properties/methods?
To use uCoolBlinkies in your projects, just add the two UserControls, uCoolBlinkies.vb and uOneBlinky.vb to your project. uCoolBlinkies should then appear (maybe after a Rebuild) in your Toolbox, and you can drag one or more to your form. (Don't drag uOneBlinky to your form. It is used only by uCoolBlinkies.)
Points of Interest
Use of a Queue and BackgroundWorker to keep track of the order of events when accessed by multiple threads. Maybe someone out there could suggest an improvement to this.
Use Threading.Timer to reliably delay a repaint event (to turn the light off), which doesn't keep a thread waiting.
History
- This is submitted first here November 30, 2009, but has been in development on and off for over 10 years. I had a lot of trouble in my multi-threaded application, there were bizarre timing issues, and it all came down to this little wee tiny blinky light not delaying properly! Now that I think I've beaten that issue and made it thread-safe, I am submitting this for use by others, and if any of the community sees any glaring or even subtle problems with my use of
Queue and Threading.Timer, I welcome your comments and contributions.
- Dec 2, 2009 - Editing clarifications made, no programming changes