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 Dequeue
ing 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 Queue
d, 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 UserControl
s, 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