Link to home
Start Free TrialLog in
Avatar of mtriviso
mtriviso

asked on

DoEvents and Stack Errors

I am writing an app that uses DoEvents. On a rare occasions I receive an Out of Stack Space error.

Is there way to clear the procedure stack and gracefully handle the error?

I need a really definitive response.
Thanks
Avatar of mark2150
mark2150

There is no graceful exit from a stack error. The internal processor state cannot be saved. you have no way of knowing what was not pushed onto the stack so there is no clean out. Any attempt to perform any other operations will generally require some additional stack space (every function call requires stack, so with no stack, you can perform no functions!)

OOS is a hard DOA.
The real answer is to remove the doevents, since they cause the stack error.
DoEvents is very dirty VB trick that can cause stack error and other crashes. Replace the doevents with timers and you will be fine.

Sample:
Old code:

sub OldSubWithDoEvents
     <Code before doevents>
     doevents
     <Code after doevents>
end sub

New code:
sub NewSubWithoutDoEvents
     <Code before doevents>
     timer1.enabled = true
end sub

sub timer1_tick
     timer1.enabled = false
     <Code after doevents>
end sub


Avatar of mtriviso

ASKER

Let me ask you this:

here is a snippet of my code that shows the DoEvent
Private Sub Inet1_StateChanged(Index As Integer, ByVal State As Integer)
    DoEvents
    Dim theString As String
    Dim vtData As Variant ' Data variable.
    Dim errorString As String
    Dim theMessage As String
    Dim theErrorCode As Long
    On Error GoTo errorHandler
    If mStopScanning = True Then
        Call stopEverything
        Exit Sub
    End If
    .
    .
    .

So, each time I the inet control raises a state changed event, I perform a DoEvents.

Is there any other way to keep the application responsive without using DoEvents?

Once the inet control detects that it's ready to receive data, it goes into a small loop (see below). This is where the app becomes unresponsive to user actions like moving windows around, switching to other apps, etcetera.  I just don't get it. How can I keep the app responsive without DoEvents? I do know that putting a DoEvents inside a loop is asking for disaster. And since there's no way to recover from a stack error (which is almost negligent on Microsoft's part), I think it's best to avoid DoEvents completely.
            .
            .
            .
            Select Case mState
                Case kGetPage
                    Call setPanels(kUpdate, "Receiving page", "", "")
                    Call setButton(kEnable, pictScanPage, kScanPageBright)
                    mPage = ""
                    gTimeSentInet1 = DateAdd("s", kTimeOut, Now)
                    tmrInet1.Enabled = True
                    vtData = Inet1(Index).GetChunk(kChunkSize, icString)
                    Do While LenB(vtData) > 0 And mStopScanning = False
                        ' App becomes unresponsive to gui events.
                        mPage = mPage & vtData
                        gTimeSentInet1 = DateAdd("s", kTimeOut, Now)
                        vtData = Inet1(Index).GetChunk(kChunkSize, icString)
                    Loop
                    tmrInet1.Enabled = False
                    .
                    .
                    .

All I want is for my app to play nice with everything else running. I thought DoEvents would allow me to accomplish this. Is there some other technique I can use?

Mike
Like I said use a timer.

To replace a loop do the following

:::Old code:::
do while <condition>
     <my code>
loop
<after code>

:::new code:::
timer1.enabled = true

sub timer1_timer
    timer1.enabled = false
    if (condition)  
        <my code>
         timer.enabled = true
     else
         <after code>    
    end if
end sub
Hmmm.

I don't get how this technique causes the gui to remain responsive.

Also, I have lots of code that handles ansynchronous activities. This stuff is hard enough to debug without adding a new layer of complexity that seems to break up the code into an nonintuitive nightmare.

I know it's not your fault that MS designs crappy tools.

I will award you the 300 points after you explain the theory behind your technique and if you can explain to me exactly what the DoEvents command does that makes it so useless.

And while you're at it tell me what you think about all this. I was under the impression that Windows was supposed to be pre-emptively multitasked. Doesn't it seem like the onus should be on the OS to handle gui events?

Thanks for all your help.



What my technique does is allowing windows to intervene. Windows is controlled by messages. When you move the mouse  or press a button, a message is send. If windows cannot send you any messages since you are running in some kind of loop then your application becomes inactive.
So your application should make sure that after it has done something that you pass the control back to windows. When you exit your subroutine, windows will take back the control and send you the next message.
This is what the timer takes care of. The timer tick will be your next event to do something and it allows windows to send you messages in between.
DoEvents process also some messages, but since your subroutine is not left yet, the stack gets bigger and bigger. Press Ctrl-L in debug mode to verify.
Replace doevents with a timer.

Compare this code.
First press button1 and type stop in the editbox. A message will appear.
First press button2 and type stop in the editbox. A message will appear.
So functionality is the same
Now press button1 and press button1 again a couple of times. Now place a breakpoint on command1_Click and look at the stack <ctrl-l>. DoEvents makes a mess of your stack.

Option Explicit


Private Sub command1_Click()
    Do While (Not testCondition)
        DoEvents
    Loop
    ConditionReached
End Sub

Private Sub Command2_Click()
    Timer1.Enabled = True
End Sub

Private Sub Form_Initialize()
    Timer1.Enabled = False
    Timer1.Interval = 1
End Sub

Private Sub Timer1_Timer()
    Timer1.Enabled = False
    If (Not testCondition) Then
        Timer1.Enabled = True
    Else
        ConditionReached
    End If
End Sub

Private Function testCondition() As Boolean
    testCondition = Text1.Text = "stop"
End Function

Private Sub ConditionReached()
    MsgBox "ok"
End Sub
Ok.

I'll accept your answer but have no plans to implement such a technique. It's simply too byzatine. Windows should implement their messaging properly.

I wonder, is there some other, lower level technique that I can use to trap and respond to requests from the OS without having to resort to basically re-structuring all my well-designed code to accommodate a flaw in the implementation of the DoEvents and the poor stack handling facilities in VB.

I've added another 15 points. That's all I have.
You must be joking. You are programming in an interactive environment. That means that you are responsible for keeping the stuff alive.
DoEvents is only available in VB. Other languages don't have this "bug" called doevents.
Anyway how most other platforms solve these issues is using multi-threading. But that is more or less impossible to implement in VB.

Here is some info about multi-threading.
Here is an example from http://www.hilonet.com/vbthread/
The code
1.Assuming that we have an application that among other things can copy a file on the click of a button.

2.The copy button could have code like this:

Private Sub cmdCopyFile_Click()
Dim lpThreadID As Long
Dim hThread As Long

'spawn a new thread of execution starting with the AsyncFileCopy function
hThread = CreateThread(ByVal 0&, ByVal 0&, AddressOf AsyncFileCopy, ByVal 0&, ByVal 0&, lpThreadID)
CloseHandle hThread 'clean up after ourselves
End Sub


3.Code this for you module:

Option Explicit

Public gsOriginalFile As String
Public gsDuplicateFile As String
Declare Function CreateThread Lib "kernel32" (lpThreadAttributes As Any, ByVal dwStackSize As Long, ByVal lpStartAddress As Long, lpParameter As Any, ByVal dwCreationFlags As Long, lpThreadID As Long) As Long
Declare Function CloseHandle Lib "kernel32" (ByVal hHandle As Long) As Long

Public Function AsyncFileCopy() As Long
'Assuming that gsOriginalFile & gsDuplicateFile
'are valid path/file names set elsewhere.

FileCopy gsOriginalFile, gsDuplicateFile
End Function

See also
http://www.microsoft.com/msj/0897/multithreading.htm

BTW: You accepted my answer, but rejected it. I don't understand.
You can try moving your DoEvents "lower" in your function - not using it as the first line in your Inet1_StateChanged, but later. This will allow processing more of your function and reduce calls stack.
You can increase your stack by using NT instead of Windows 95.
As a replacement for doevents, sometimes, I use Sleep API - it will allow more time to OTHER applications. I think it is better then Mirkwood's Timer solution.
Most reasons for stack fault in my experience, is because of large arrays or strings defined locally, in procedures. So, I move my big strings/arrays declarations to module level, even though they are used only locally in a procedure.
You should do the same with declaration of vtData!
I appreciate the help both of you have provided.

I like ameba's solution better though.

I would like to split the points between the two of you but don't know how.

Also, it simply amazes me that the DoEvents is such a piece of crap. The idea of having to declare a variable that should be local as a module level var is repugnant. I am beginning to think that VB is sizzle than steak!

So this is what I am going to do. I am going to declare the vtData as a module level var and place the DoEvents lower in the method.

Thanks for all the help.

Again, I would like to split the points between the two of you. If you can figure out how we can do that it would be great.
You can ask Linda to split the points at the forum "Experts Exchange"
BTW: have you tested ameba solution. I mean. Sleep gives other applications process time. Not yours. Your application is not respondsive at all. Sleep is only usefull, if you want to pause your application.

Here is a modified sample. Pressing button 3 will hang your application.

private Declare Sub Sleep Lib "kernel32" Alias "Sleep" (ByVal dwMilliseconds As Long)

Private Sub command1_Click()
    Do While (Not testCondition)
        DoEvents
    Loop
    ConditionReached
End Sub

Private Sub command3_Click()
    Do While (Not testCondition)
        sleep 1000
    Loop
    ConditionReached
End Sub


Private Sub Command2_Click()
    Timer1.Enabled = True
End Sub

Private Sub Form_Initialize()
    Timer1.Enabled = False
    Timer1.Interval = 1
End Sub

Private Sub Timer1_Timer()
    Timer1.Enabled = False
    If (Not testCondition) Then
        Timer1.Enabled = True
    Else
        ConditionReached
    End If
End Sub

Private Function testCondition() As Boolean
    testCondition = Text1.Text = "stop"
End Function

Private Sub ConditionReached()
    MsgBox "ok"
End Sub
mtriviso, thanks for the points.

And my solution is not "sleep". I didn't say it is.
Yours "timers": Do you really use timers that much?
no problem.

I use timers when I need them. In this case there is a setting that allows the user to set how long they want to wait before timing out. So, I use a timer to handle it.

Using a mod level var seems to have made things better. And placing the DoEvents later in the method seems better.

I have only gotten two stack errors in four months of development so hopefully the tricks you guys mentioned will make them go away completely.
Hi, mtriviso
To split points, go to Customer Service, Forum "Expert Exchange" and ask a question, like:

I would like to split points between Mirkwood and ameba for question: Q.10099097
Thank you

Thank you
Hi. I did this and was told the points had been split. If you didn't receive your portion, you will probably have to take it to the admins. I posted my request in the forum,  "Experts Exchange" and asked Linda to do the point split.
ASKER CERTIFIED SOLUTION
Avatar of covington
covington

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Hello

Im obviously a bit late for this discussion however I have a similar application running without any problems and using DoEvents. The trick is not to put it in the StateChanged event as this event is fired very frequently particulary when downloading. You should perform your Select Case routine in the command that initializes the download or whatever youre doing and leave StateChanged to set the Public variables which your main routine will act upon eg:

      kk$ = Inet1.OpenURL(thisurl)
      Do Until GotResults
        DoEvents
      Loop

The simple rule is that Doevents should never be used in  events which are fired automatically and are beyond your control.


Hope this helps