DoEvents Strikes Back – Part 1
I have needed to butt heads with a colleague over their excessive use of DoEvents throughout their code. I have seen many times DoEvents used as a crutch for poor performing code. The form isn’t refreshing and the user has no idea what is happening, DoEvents. Unable to have a cancel button working, DoEvents. A loop taking too long to process, DoEvents.
First things first. DoEvents was used as a mechanism to process windows message events on the queue for the current application. Its main use, and it should have been always for this is the implementation of long running processes and processing a cancel mechanism for the process. But I have seen it in non-looping processes, like in the case for me butting heads, and certainly other places, where it looks like they are trying to implement some visual multi-threading (by that I mean the user might think the application is multi-threaded when it isn’t.
Now DoEvents in the .NET Framework is implemented but is implemented differently from the one in VB6. Though my main issue is it’s use in VB6 when it isn’t fully understood why it is there. This annoyed me so much so that I eventually wrote a version of my own that is the same as the one implemented in .NET.
Private Sub DoAction() Dim i As Long Me.StatusBar.Value = 0 For i = 1 to 10000000 ‘(yes 10 million) PerformActions i If i Mod 100000 = 0 then Me.StatusBar.Value = Me.StatusBar.Value + 1 End If Next i End Sub
Take a look at this code. The application looks through, it increments a status bar on the form. If the PerformActions is firing events and handling them, the refresh and update events on the form will not be executed until all events are processes.
Private Sub DoAction() Dim i As Long Me.StatusBar.Value = 0 For i = 1 to 10000000 ‘(yes 10 million) PerformActions i If i Mod 100000 = 0 then Me.StatusBar.Value = Me.StatusBar.Value + 1 End If DoEvents() Next i End Sub
The above code shows the common place for DoEvents. In the above case, DoEvents is executed 10 million times. this is possibly a little extreme. The best place for DoEvents is in the PerformActions method, because it can be then selectively executed IF other events need to be processes.
Some might question what is wrong with the position of DoEvents in the above code. It is the sleeping side effect that happens. DoEvents is called, it then starts processing the events on the queue. Once it returns it calls the Sleep API function, to end out the time slice give to it by the operating system. The main problem is, the time slice is given is dependant on what needs to be processed. If there is nothing on the queue, there is still a moment of sleeping, milliseconds, sure, but multiply milliseconds by 10 million and it might be significant.
And to give you an example. The time to execute code the first example above, the one without DoEvents, it takes 0.389 seconds. Not too bad for running a loop 10 million times
Then the second examples takes 3.56 seconds. 10 times longer, why, all because of DoEvents. And this would be greater if there is actually events to be processed. Don’t believe me. Take both pieces of code and include a
Dim start As Single Dim finish As Single start = Timer For i = 1 To 10000000 ... Next i finish = Timer MshBox finish - start
Include the above code in and it will show you the difference between both methods. In these cases, PerformActions is merely adding the 1 to the number that is passed in.
So if I am saying how bad DoEvents is, what can be done. If for whatever reason, this is the dialog.
So I have this, I click Click Me, something happens and I can click Stop to stop it processing. Problem, which I have mentioned is, since the Click Me event is running, this event will continue to process until complete and no other event on the form will process while this happens. So I can’t click Stop to stop it even if I wanted to.
But with this, why. Get rid of the button, remove the check to see if I need to stop processing, since it takes .3 of a second, it isn’t long enough to really add all that additional code in there to enable the process to be stopped.
But I am not here to explain when is a good time to use it. In reality I did show the reason for it, but in reality, I did also state is it needed. I would rather remove it completely from the code and save myself 2.7 seconds than provide an ability for the user to cancel a very quick process.
So, if using DoEvents is not good what is there as an option? Roll your own. In the early days of VB it was all about the languages not having enough features and therefore you needed a little from the Win32API to improve controls or applications to give them features that they were used to, but didn’t exist in older versions of controls.
But I have a better solution. How much better, a lot.
'API Point Structure Private Type PointAPI x As Long y As Long End Type 'Windows Message Structure Private Type winMsg hwnd As Long message As Long wParam As Long lParam As Long time As Long pt As PointAPI End Type 'API declaration Private Declare Function PeekMessage Lib "user32" Alias "PeekMessageA" (lpMsg As winMsg, _ ByVal hwnd As Long, ByVal wMsgFilterMin As Long, ByVal wMsgFilterMax As Long, _ ByVal wRemoveMsg As Long) As Long Private Declare Function TranslateMessage Lib "user32" (lpMsg As winMsg) As Long Private Declare Function DispatchMessage Lib "user32" Alias "DispatchMessageA" (lpMsg As winMsg) _ As Long Private Const PM_NOREMOVE = &H0 Private Const PM_REMOVE = &H1 Private Const PM_NOYIELD = &H2 'Only usable on Windows 2000 and greater Private Const PM_QS_INPUT = &H70000 Private Const PM_QS_PAINT = &H200000 Private Const PM_QS_POSTMESSAGE = &H980000 Private Const PM_QS_SENDMESSAGE = &H400000 Public Sub ProcessWindowsMessages(Optional ByVal hWnd As Long = 0) Dim currentMessage As winMsg 'Look at the messages on the queue. We are processing them all for the current window, 'or all on this thread. Do While Not PeekMessage(currentMessage , hWnd , 0, 0, PM_REMOVE) = 0 'Convert any virtual keystrokes into real ones, if there are any, and process them TranslateMessage currentMessage 'Process the message DispatchMessage currentMessage Loop End Sub
Well that is a lot of code to replace a small pre-defined function DoEvents. Yes, but if you take that code, put it in a module, then replace the DoEvents with ProcessWindowsMessages you will find a very good increase in time.
Here is my complete code listing for the form.
Private booStop As Boolean Private Sub cmdButton_Click() Dim i As Long Dim start As Single Dim finish As Single Me.StatusBar.Value = 0 start = Timer For i = 1 To 10000000 'Check to see if the stop button was pressed. If booStop = True Then Exit For End If 'perform the actions needed PerformActions i 'Increment the status bar if it is time If i Mod 100000 = 0 Then Me.StatusBar.Value = Me.StatusBar.Value + 1 End If 'Do Events ProcessWindowsMessages Next i finish = Timer MsgBox finish - start End Sub Private Sub PerformActions(number As Long) number = number + 1 End Sub Private Sub cmdStop_Click() booStop = True End Sub Private Sub Form_Load() booStop = False End Sub
So I have included the timers in this full listing, which is OK, but I have the ProcessWindowsMessages procedure all.
So, try it for yourself. In both ProcessWindowsMessages and DoEvents cases, the stop button works. (Yes I included some additional information on the dialog box)
Stop was pressed, but you need to be quick considering how long it takes to process the 10 million iterations.
Using DoEvents, means there is more time for me to click the button to stop processing.
And with this, I can’t even click the button, not that I would have time to in honesty to even click a stop button.
I have presented the facts, VB6 and DoEvents has been a blight on a lot of my coding career and trying to see and teach people there are better ways of doing things. In Part 2 I will explore the other methods in which you can process long running requests without the need to implement DoEvents or even ProcessWindowsMessages but still maintain a responsiveness to the calling application.