The comment stripper FSM works okay, but it has a dangerous flaw. It's a flaw that is inherent in event-driven systems, and one that also crops up in regular Visual Basic programs.
The problem is re-entrant code, and you might have come across it when working with data controls, Form_Resize events, or code that uses DoEvents. Let's have a look at a simple example of
re-entrancy using a data control. The program shown in Figure 8
is about as simple as it gets, with a single data-bound list wired up through a data control to the Visual Basic sample database BIBLIO.MDB. Assume that the list contains a set of records we need to process somehow and that it doesn't matter in which order the records are processed. Clicking in the list causes a Reposition event, and the program puts up a message box that lets us simulate the kind of Jet page-locking error we might encounter in a multi-user application. You can think of the Reposition event handler as the equivalent of the
DoFSM function in the comment stripper program. Here's the important bit: Private Sub datBooks_Reposition() If MsgBox (…) = vbYes Then
datBooks.Recordset.MoveNext EndIf End Sub
Figure 8: Recursion in the data control's Reposition event Clicking No simply continues, and this is where we'd process the
new record. Clicking Yes simulates a locking error and skips to the next record by calling the MoveNext method of the data control's recordset. The idea is that we'll reach the end of the
locked page after skipping a few records and so find a record we can process. The problem here is that we're calling MoveNext from within the Reposition event handler, which causes another
reposition event before the first one has finished – this is recursion. The example program maintains a static variable to count the number of recursions; the count is displayed in the
message box, and you'll note that the program also prints the entry and exit traces for the reposition event to the Immediate window. This example, which comes from a real program, might not have
particularly serious consequences because it's a pure recursion that doesn't nest too deeply, and it involves no static data (except for the counter, of course). Generally, however, and
particularly when we're devising code such as FSMs to control the loading and unloading of forms, the code will break as soon as we try to invoke it recursively. We might, for example, end up in a
situation in which you're trying to load a form from its own Form_Load event. Coming back to the recursive Visual Basic program, it's not immediately obvious how to fix the problem. It turns out that
this is quite a common class of problem, and one that conveys the true flavor of event-driven code. What we want to do when we find a lock is to exit the event handler and then immediately issue a MoveNext
on the recordset. Unfortunately, Visual Basic can't do this because as soon as we exit the event handler, control passes back to the run-time system (the <Non-Basic Code> we see when we
select View/Call Stack in break mode). What we need to be able to do is to post some kind of request for a MoveNext and have it execute after we've left the Reposition event handler.
Just because Visual Basic won't do this kind of thing for us doesn't mean that we can't implement it ourselves. By modifying the pathological data control program to use a simple event queue
we can achieve what we need. We'll use an unsorted list box as a convenient event queue (inelegant, but if we make it visible it's a good debugging aid) and a timer control that continually polls
the queue looking for events. There's only one kind of event in the program, so we don't even need to look at its value when we find it on the queue – we always just consider it a request for a MoveNext
. Here are the bones of the fixed program: Private Sub datBooks_Reposition() If MsgBox (…) = vbYes Then PostEvent ("EventId")
End If End Sub
Private Sub PostEvent(ByVal sEvent As String) ' We're using a list box as a queue. The 'sorted'
' property must be set to False. We only have a single ' event in this program, so it doesn't matter what the
' value is. If you want to see what's happening, make ' the list box is visible and set the timer interval to ' something like 500ms.
lstEventQueue.AddItem sEvent End Sub
Private Function bGetEventFromQueue() As Boolean ' Retrieve an event from our list box queue. The design
' of this program means there will only ever be one event ' queued, but this code is general so you can experiment. If lstEventQueue.ListCount = 0 Then
bGetEventFromQueue = False Else lstEventQueue.ListIndex = 0
lstEventQueue.RemoveItem 0 bGetEventFromQueue = True End If End Function
Private Sub timQueueManager_Timer() ' This is where the MoveNext actually gets done. Timer ' events aren't re-entrant, no matter how small we make
' the interval. Our MoveNext operations, and hence our ' Reposition events, are therefore guaranteed to be ' sequential.
If bGetEventFromQueue() Then datBooks.Recordset.MoveNext End If End Sub
The program works like this: inside the Reposition event, instead of directly calling MoveNext when a locked record is encountered, we post an event onto the queue and then exit the event handler.
The queue manager (the timer control) then comes along and, finding an event on the queue, kindly calls MoveNext for us. Now, however, the MoveNext is called from the timer's event handler,
and there's no recursion. Notice that it doesn't matter how fast we push event requests into the queue; we never get recursion because the events are processed one by one in sequence. Adding an Event Queue to an FSM To prevent re-entrant code, we need to add a queue to the FSM model. Strictly speaking, the comment stripper program doesn't
need a queue because it doesn't do anything that will cause recursion. Because it's an example program, however, we'll add the queuing now so that we can build on it when we design real-world FSM programs later.
The queue built in the previous example worked adequately, but it needed a form to carry the list box and the timer control. This awkwardness over essentially non-visual code has dogged Visual
Basic from the start, and it means, for example, that we can't define a queue inside a class or a startup module without creating a dummy form. We could dump the controls onto an
existing form, of course, but that's anathema to modular design, and it means we must contrive to load the form before starting the event queue. Getting rid of the list box isn't too hard, but
until Visual Basic 5 there was no getting around that timer control without doing something horrific like this: Sub Main() Dim nEvent As Integer
frmMain.Show vbModeless ' Main program is in here. Do If bGetEventFromQueue(nEvent) Then
DoFSM State, nEvent End If DoEvents
Loop End Sub With Visual Basic 5 and 6, however, we can at last devise acceptable code-only solutions to this kind of problem – in this case, to build an event queue. By using the AddressOf
operator, we can call the SetTimer API function and pass a Visual Basic routine as the timer's callback procedure. This means we can create a timer from pure code, and just like a Visual Basic Timer
control, it will invoke the Visual Basic procedure asynchronously at the requested interval. Creating a timer is simple: lTimerId = SetTimer(0&, 0&, 500&, AddressOf MyFunc)
The first two parameters are NULL values, which simply signify that the timer isn't associated with any window, and the third is the timer interval, in milliseconds. The last parameter is the
interesting one; it passes a pointer to a Visual Basic function that will be invoked by Windows whenever the timer fires. Windows expects this function to have the following interface and to pass
the appropriate parameters: Sub MyFunc(ByVal lHwnd As Long, _ ByVal nMsg As Long, _
ByVal lEventId As Long, _ ByVal lTime As Long)
For now, just ignore the parameters. We also need to make sure we destroy the timer when we're finished with it: Call KillTimer (0&, lTimerId) That takes care of the queue manager, so now all we need to do is
provide a queue for it to manage. A simple way to do this is to use a Visual Basic collection: Private m_colEventQueue As Collection We'll see a more sophisticated use of collections later, but for
now we can use one as a simple queue by defining a couple of routines: Sub AddEventToQueue(ByVal nEvent As Integer) m_colEventQueue.Add nEvent End Sub
Function bGetEventFromQueue(ByRef nEvent As Integer) As Boolean If m_colEventQueue.Count = 0 Then bGetEventFromQueue = False Else
nEvent = m_colEventQueue.Item(1) m_colEventQueue.Remove 1
bGetEventFromQueue = True End If End Function And that's it – a code-only asynchronous queue manager that we
can build into a class or a normal module. Here's the code for the event queue. This code is actually designed to handle multiple concurrent FSMs; the class CFSM it refers to is the class we're building here.
Sample Code: Event Queue This code wasn't included in Advanced Visual Basic 6.
Class CEvent Public Destination As CFSM Public EventId As Long
BAS module GEventQueue Option Explicit
Private Declare Function WinKillTimer Lib "user32" Alias "KillTimer" (ByVal hwnd As Long, ByVal nIDEvent As Long) As Long
Private Declare Function WinSetTimer Lib "user32" Alias "SetTimer" ByVal hwnd As Long, ByVal nIDEvent As Long, ByVal uElapse As Long, ByVal lpTimerFunc As Long) As Long
Private m_lTimerId As Long Private m_colEventQueue As Collection
Private m_nInstances As Integer
Public Sub AddInstance() If m_colEventQueue Is Nothing Then StartQueue
m_nInstances = m_nInstances + 1 End Sub
Public Sub RemoveInstance() If m_nInstances < 0 Then m_nInstances = 0
m_nInstances = m_nInstances – 1 If m_nInstances = 0 Then StopQueue End Sub
Public Sub PostEvent(ByVal oFSM As CFSM, ByVal nEvent As Integer)
Dim oNewEvent As CEvent: Set oNewEvent = New CEvent Set oNewEvent.Destination = oFSM oNewEvent.EventId = nEvent
m_colEventQueue.Add oNewEvent End Sub
Public Sub FlushQueue(ByVal oFSM As CFSM) Dim nThisEvent As Integer
nThisEvent = m_colEventQueue.Count Do While nThisEvent >= 1 If m_colEventQueue(nThisEvent) Is oFSM Then
m_colEventQueue.Remove nThisEvent End If
nThisEvent = nThisEvent – 1 Loop End Sub
Private Sub StartQueue() Set m_colEventQueue = New Collection
m_lTimerId = WinSetTimer(0&, 0&, 10, AddressOf Dispatcher) End Sub
Private Sub StopQueue() Set m_colEventQueue = Nothing
WinKillTimer 0&, m_lTimerId End Sub
Private Sub Dispatcher(ByVal lhwnd As Long, ByVal lMsg As Long, ByVal EventId As Long, ByVal lTime As Long)
Dim oEvent As Cevent Set oEvent = GetEventFromQueue If Not (oEvent Is Nothing) Then
oEvent.Destination.Dispatcher oEvent.EventId End If End Sub
Private Function GetEventFromQueue() As CEvent
If m_colEventQueue.Count = 0 Then Set GetEventFromQueue = Nothing Else
Set GetEventFromQueue = m_colEventQueue(1) m_colEventQueue.Remove 1 End If End Function |
NOTE: in Advanced Visual Basic I went on to include instructions for building an event queue using the PostMessage API function to
send messages to an invisible window created with CreateWindowEx. In practice this doesn't really add anything useful to the queue we made with SetTimer, and actually it's a complete pain in the
neck to work with because subclassing windows is very dangerous in the Visual Basic IDE. Next... |