Results 1 to 5 of 5

Thread: App gets too busy for user input (VB2015)

  1. #1

    Thread Starter
    New Member
    Join Date
    Nov 2017

    App gets too busy for user input (VB2015)

    Hi guys

    I've been writing a 6502 CPU cross compiler and simulator off and on over the last few years. It's now pretty much out of alpha and into beta.
    One issue that I'm facing is with the simulator which, when it runs the 6502 compiled code (in a loop) then background tasks (suh as checking for user input) seem to get dropped. This onlt affects my appplication.
    I've tried to alleviate this using DoEvents but this only works for 30 seconds and then user input is then ignored again.
    Reading around the internet it seems this is down to a message queue becoming saturated or ignored?
    I have also read that a backgrounder worker would be a better solution. I tried that but found that my code (running from a module) could not seem to access any form controls from other forms - a major deal killr.

    Can anyone suggest a way of allowing user input to be picked up and cause my simulator loop to quit? I don't mean exiting the loop, but just getting user input to register so thar a cancel flag can be set.
    Please keep in mind that I'm defo at hobby level understanding here so you may need to explain things quite bascally (no oun intended)

  2. #2
    Super Moderator Shaggy Hiker's Avatar
    Join Date
    Aug 2002

    Re: App gets too busy for user input (VB2015)

    I would say that you were close. I'm a bit surprised that DoEvents would stop after a certain amount of time, as I've never seen that happen before, but DoEvents can have other side effects that are undesirable, so it's far from an ideal solution.

    BackgroundWorker is closer, but not ideal...well, you've kind of figured that out already, but I'd say it isn't ideal for a different reason.

    Running the compiled code in a loop, if done on the UI thread, would certainly cause you trouble. I suspect that you could get it to work if you arranged it correctly and used DoEvents, but a different solution would likely be better. The point is that the loop will block the UI thread from processing any messages until the loop ends. Since all user interaction comes in the form of messages posted to the message queue, the fact that the UI isn't able to process messages means that no interactions are processed, so the app looks frozen.

    The best solution is probably to run the loop in a different thread. BackgroundWorker does work on a different thread, but it's really just a way to run a thread with a few extra conveniences. The drawback is that it is a component that generally is used on a form. If there is one form that makes sense for that loop, then the BGW could still make sense. It doesn't sound like that's the case, though, and it doesn't feel right to me. It seems to me that you want to create an actual thread, and have the loop running in that thread.

    The drawback to threading of any sort is that you have to be pretty careful about interacting with any UI elements. Without understanding better what happens in that loop, it isn't clear whether that even IS an issue, though it sounds like it is.

    At that point, I couldn't figure out how to write the next part. Basically, there are plenty of potential problems, and discussing them in general terms doesn't seem useful. If you did anything with the BGW, and have any code that you can show, that would be good to see. It just isn't clear to me how the emulation loop is supposed to interact with forms.
    My usual boring signature: Nothing

  3. #3
    You don't want to know.
    Join Date
    Aug 2010

    Re: App gets too busy for user input (VB2015)

    It's easier to explain with an example, but let's talk about it. I think you can structure your application in a way that perfectly fits this, but you're going to have to do some work.

    You can't really use any of the "helpful" tools for threading for this. The Thread Pool, BackgroundWorker, and Task APIs were all designed for "short" tasks, things that will finish within a few seconds (or at all). Your program loop may run "forever" if the user doesn't ever click "Stop". So it's a poor fit for these threading models. That means you have to use a Thread, because that's good for the case where you want the work to be able to continue "forever".

    Like Shaggy Hiker says, this creates a problem in GUI applications. Controls have a thing called "thread affinity". It means some things important to them are stored specifically on the thread that created them. So if you try to work with a control from the wrong thread, lots of bad things can happen. You'll know you did this in debugging because usually VB throws an exception and warns you. In the wild, you'll see symptoms from "crashes" to "nothing happens".

    To get around that, you have a simple tool. Every Control has an InvokeRequired property. It is True if you are on the wrong thread and False if you are on the right thread. Every Control also has an Invoke() method. It takes a "delegate", which is "a Sub/Function crammed in a variable". It will send that delegate over to the "right" thread, then execute it. These are the only two safe things you can do with a control from ANY thread.

    So, when you are writing your thread code, keep an eagle eye out for when you try to update some control. The moment you do, you need to write a special method to do whatever. For example, if you wanted to change the text in a TextBox, you should write:
    Sub UpdateTextBox(ByVal theBox As TextBox, ByVal newText As String)
        If theBox.InvokeRequired Then
            theBox.Invoke(Sub() UpdateTextBox(theBox, newText))
            theBox.Text = newText
        End If
    End Sub
    Top to bottom, in English, this reads: "If the text box says we are on the wrong thread, tell it to please ask the UI thread to call this method again with the same parameters. If the text box says we are on the right thread, change its text."

    If visualization is your thing:

    Making a thread is "easy". You put the code you want to run in the thread into a Sub, then tell the Thread class to use that Sub and start the thread. If the Sub exits, the Thread stops running. Since you want a "stop" button, this functions kind of like a Cancel, which is a common pattern. In sort-of-pseudocode we can envision it like:
    Public Class SimulationThread
        Private _shouldCancel As Boolean
        Public Sub Start()
            <start a thread running DoWork()>
        End Sub
        Private Sub Work()
            While <instructions are left>:
                If _shouldCancel Then
                    Exit While
                    <execute an instruction>
                    <update the UI>
                End If
            End While
        End Sub
        Public Sub Stop()
            _shouldCancel = True
        End Sub
    End Class
    When you call Stop(), the flag is set to False. The next instruction loop notices and quits the While loop that executes instructions. Then, maybe some post-execution code runs, and the thread's done.

    "Update the UI" can involve a lot of things, but I think we can make fairly quick work of it using a "state" class, shuttling that over to the UI, then updating each control once we're on the right thread.

    I've attached a project that, while not a 6502 simulation, ought to give you some ideas. It simulates a very simple CPU with no RAM and an "A" register. The only instructions are "inc" which increments A and a jmp. For simplicity, the "code" that's running is:
    jmp 0
    An infinite increment loop. Yay.

    The first most important part is the Cpu class:
    Imports System.Threading
    Public Class Cpu
        Private _isRunning As Boolean
        Private _shouldStop As Boolean = False
        Private _listing As Listing
        Private _state As CpuState
        Public Property State As CpuState
                Return _state
            End Get
            Private Set(value As CpuState)
                _state = value
            End Set
        End Property
        Public Sub New()
            State = New CpuState(0, 0)
        End Sub
        Public Sub Start(ByVal listing As Listing)
            _shouldStop = False
            _listing = listing
            Dim t As New Thread(AddressOf RunListing)
        End Sub
        Private Sub RunListing(ByVal unused As Object)
            ' Let the UI know the listing has started.
            _isRunning = True
            While Not _shouldStop
                Dim nextInstruction As Instruction = _listing.Instructions(_state.IC)
                State = nextInstruction.Execute(State)
                ' Let the UI know the state has changed.
            End While
            _isRunning = False
            ' Let the UI know the listing is finished.
        End Sub
        Public Sub [Stop]()
            _shouldStop = True
        End Sub
        Private Sub RaiseCpuUpdate()
            RaiseEvent CpuUpdate(Me, New CpuUpdateEventArgs(State, _isRunning))
        End Sub
        Public Event CpuUpdate As EventHandler(Of CpuUpdateEventArgs)
    End Class
    It knows if it is running, and if it should be stopping. It keeps track of a "listing", which is what I called the instructions it will run. It keeps track of a CpuState object that represents the IC and register for the imaginary CPU.

    When you call Start(), it wants a Listing so it knows what program to run. It starts a Thread using the RunListing() method. It has an 'unused' parameter because you have to have a parameter for Thread, but you don't always use it. Maybe I should have passed the Listing to it. Whatever.

    The most important thing here is the CPU raises an event any time something about its state changes. That means if the IC or a register changes, or if _isRunning changes. The real answer here is "any time you might want to update the UI, it should raise this event". The CpuUpdateEventArgs captures the _isRunning flag and the current CpuState so the UI can update. You could try to access the State property of the CPU, but since it's being updated by a thread there's some possibility you'll ask for values at a bad time and get inconsistent data. We'll come back to that in a minute.

    Why did I Sleep for 1s? Well, if we run this as fast as we can, we'll raise so many events it will lock up the UI. If you want to run the 6502 with a simulated clock speed, you might consider "Only raise the event if x cycles have elapsed" to help not flood the UI. My rule of thumb is to try not to raise events more often than 250ms apart.

    If you call Stop(), _shouldStop is set. The next time the instruction loop spins, it will notice this and quit. So _isRunning is set to False and a final update is raised. Then the thread dies unceremoniously.

    It's important to understand something about CpuState:
    Public Class CpuState
        Public ReadOnly Property IC As Byte
        Public ReadOnly Property RegisterA As Byte
        Public Sub New(ByVal ic As Byte, ByVal registerA As Byte)
            Me.IC = ic
            Me.RegisterA = registerA
        End Sub
    End Class
    I designed it to be a special kind of type we call "immutable". That means once you set IC and RegisterA, they cannot change. Why did I do this?

    Imagine if we've got 6 registers, and we're trying to update the UI while the thread is executing instructions. The UI asks for A and updates. Then it asks for B and updates. Then the program changes B. The UI asks for C and updates, and so on. The program updates D. The UI asks for D and updates. And so on. We now have the wrong value for B. The thread updated B after we asked for it, and we don't know we're supposed to be asking again.

    This is a common problem in threading. There are two common ways to solve it. One is to use "synchronization constructs". These are special bits of code able to enforce rules like, "Only let one thread access this part of the code at a time." The most common one is called SyncLock because it's easy: the other constructs ask you to think a lot harder and are for somewhat advanced scenarios.

    Some people don't like using SyncLock "too much". One problem is it means if your UI takes a while to update, it will make your simulation thread stop while the UI's updating. If you care about accurate CPU timing, that will throw you off by unpredictable margins. Another problem is you can simply forget it needs to be in a place, and one mistake can be disastrous.

    So the other approach is to make all of your thread data structures immutable. That means the only way the thread can change the data is to do it all at once. And if something else asks for the data, it's always true "at the instant data was collected, these were the right values". So we can't end up in a situation where we have an "old B but new D", etc. This usually all but eliminates the need for synchronization constructs, and means you work less hard.

    So whenever Cpu raises its CpuUpdate event, it passes a copy of its most current state to the UI. The UI is free to use that and take as long as it wants: the CPU will keep going and publish another update if it needs to. This is why earlier I said you shouldn't let the UI access the State property. (In fact, one could argue I shouldn't have made it a public property.)

    Finally, let's look at the UI code:
    Public Class Form1
        Private _cpu As New Cpu()
        Private _hasClosed As Boolean = False
        Public Sub New()
            ' Add any initialization after the InitializeComponent() call.
            AddHandler _cpu.CpuUpdate, AddressOf WhenCpuUpdates
            btnStop.Enabled = False
        End Sub
        Private Sub btnStart_Click(sender As Object, e As EventArgs) Handles btnStart.Click
            Dim instructions() As Instruction = {
            Dim listing As New Listing(instructions)
        End Sub
        Private Sub btnStop_Click(sender As Object, e As EventArgs) Handles btnStop.Click
        End Sub
        Private Sub WhenCpuUpdates(ByVal sender As Object, ByVal e As CpuUpdateEventArgs)
            ' WE ARE NOT ON THE UI THREAD YET! Also, we have to worry about if
            ' the Form has closed/is closing.
            If Not _hasClosed Then
                Me.Invoke(Sub() UpdateUi(e))
            End If
        End Sub
        Private Sub UpdateUi(ByVal updateData As CpuUpdateEventArgs)
            If _hasClosed Then
            End If
            ' Make sure the buttons are set up right for the current status of
            ' the CPU.
            btnStart.Enabled = Not updateData.IsRunning
            btnStop.Enabled = updateData.IsRunning
            ' Update register indicator(s).
            txtRegisterA.Text = updateData.State.RegisterA.ToString()
        End Sub
        ' When the form closes, the thread keeps going. So make sure to stop the thread
        ' and also remove the event handler to avoid trouble.
        Private Sub Form1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles Me.FormClosing
            _hasClosed = True
            RemoveHandler _cpu.CpuUpdate, AddressOf WhenCpuUpdates
        End Sub
    End Class
    It has a Cpu instance, and it has a "_hasClosed" flag I'll have to explain later. When it's created, it creates the CPU and registers for the CpuUpdate event. It also disables the Stop button, since there's no point in letting it stay enabled.

    When you click the Start button, a Listing is passed to the Cpu's Start() method, and that starts the CPU.

    This happens to cause a CpuUpdate event to be raised, so let's look down at WhenCpuUpdates(). The capital letters are right: this is raised on the worker thread so we need to use Invoke() to get safely onto the UI thread. Let's ignore the "closed" issue and look at UpdateUi().

    This gets called via Invoke(), so it must be on the UI thread. It checks _isRunning to decide how the buttons should be enabled, then it updates the TextBox that displays register 'A'. Since it knows it has a copy of the CpuState, it doesn't have to worry about clashing with the CpuThread. Nice. What happens if it's really slow and another update happens while it's working? Well, keep in mind WhenCpuUpdates() uses Invoke() to call this. The UI thread can only do one thing at a time. So if the UI thread is busy, the next UpdateUi() call will "wait in line". Thus, updates will always happen in order.

    (But if it takes longer to update the UI than to execute an instruction, you should make sure to slow down the rate of raising the event as discussed before.)

    If you click the Stop button, it calls the Cpu's Stop() method.

    Finally, let's talk about why I give a flip about when the form is closing:

    Invoke() is a method on controls, and if the form is closed it disposes of all of its controls. But the thread is still running, and may try to raise its CpuUpdate event. If it does that after we've closed the form, The call to Invoke() will fail because the Form has been destroyed.

    So when we close the form, I set a flag to indicate it's closed. Then I try to remove the CpuUpdate event handler and stop the CPU. It's important to do all of these things, because there's another thread running. It might raise an update event WHILE we're trying to stop it. That will likely manage to call Invoke(), which causes UpdateUi() to "wait in line" for us to finish with our FormClosing handler. When it gets to run, it will see we've set _hasClosed to True and avoid doing anything dangerous. Meanwhile, we've asked the CPU to stop and also quit listening for its event, so if it raises the event again it won't cause another cycle.

    I probably should've used a SyncLock here to make sure it all happens exactly in the order I want, but I didn't manage to trigger any bad behaviors. Your mileage may vary. There's ways to make it so Cpu.Stop() will "freeze" the UI thread until the CPU actually stops. That's probably the most sane thing here, but it makes everything more complex.

    This is a demo of the basic architecture. You'll have to do a lot to expand it to a full 6502 simulation, and it will involve some careful thought. But this ought to be more than a "gentle nudge".
    Attached Files Attached Files
    This answer is wrong. You should be using TableAdapter and Dictionaries instead.

  4. #4

    Thread Starter
    New Member
    Join Date
    Nov 2017

    Re: App gets too busy for user input (VB2015)

    Thank you very much for the detailed explanation and help - both posts are amazing and much appreciated. I'm going to be using the examples here to do some tests in a separate project to familiarise myself with how threading works.

    I have to say at this point that I managed to somehow get DoEvents() working, although I'm not sure how. I have been populating the main loop with code for each 6502 instruction. It could be that perhaps I had introduced a bug there and then fixed it, though it's a tad odd that things were fine for a set period of time then it stopped working.
    Either way, given the antipathy I have sensed from many with regards DoEvents, I won't be keeping that and will port to separate threading.

    With regards my code here is a sample. It's not the whole thing as that would be waaay too long as I have finished adding the 6502 instructions in to the main loop - there are hundreds of them. I've left a couple in to show what's been done as an example though.

    Lastly: My code is a mess and I know it. At least with regards to comments and variable names. This will be cleaned up in due course, but if you spot anything I can be doing in a better way then feel free to share . Oh and for some reason the preview shows the formatting to be out. i.e. indentation. Looks fine on VB though.

        Public Sub RunCode()
            Dim iCode As Byte, iIndex As Integer
            Dim sCode As String
            Dim msgButton As Integer
            Dim sr As Byte
            ' reset the registers
            ' set the program counter (PC) which is obtained from formmain.textbox_varP
            If LTrim(Microsoft.VisualBasic.Left(FormMain.TextBox_VarP.Text, 1)) = HexChr Then
                Status.PC = Convert.ToInt32(LTrim(Microsoft.VisualBasic.Mid(FormMain.TextBox_VarP.Text, 2)), 16)
                Status.PC = CLng(FormMain.TextBox_VarP.Text)
            End If
    		' disable controls on formmain so that we limit the user while running the compiler
            With FormMain
                .RichTextBox_Work.Enabled = False
                .TextBox_VarP.Enabled = False
                .MenuStrip1.Enabled = False
                .ComboBoxHexChar.Enabled = False
                .ComboBoxBinChar.Enabled = False
                .TextBoxDebugStepping.Enabled = False
            End With
    		' display some information on the richtextbox used to display mesages and debug info
            TXTopcode(Chr(13) & "6502CA Code Simulator [alpha]", True, True, "b")
            TXTopcode(Chr(13) & "You may press ESCAPE to quit the running code at any time." & Chr(13) & "Code start address &" & sGetWord(Status.PC) & Chr(13) & Chr(13) & "<Running>...")
    		' refresh the debug richtextbox and tell the simulator that it's running (Running.status)
            Running.Start = Now.Ticks
            Running.Status = True
            ' push the reset vector address to the stack. This is so that if an RTS is executed and nothing else is on the stack then
            ' the simulator will know that the 6502 program wishes to exit. The program itself simply needs to do an RTS without having
            ' an outstanding JSR and other data having been read of the stack.  The program can also exit at any time by, itself, pushing
            ' &FFFC to the stack and executing an RTS.
            ' This is the 6502 program loop. This loop runs through, executing 6502 code in RAM() until the program is aborted
            ' or an RTS is reached which causes &FFFC (the 6502 reset vector address) to be fetched off of the stack.
                pc = Status.PC
                ' pc is the current program pcounter, status.pc is the address of the next instruction
                RunCodeParamData.NumDataBytes = -1
                ' get the command/instruction byte to be processed
                iCode = RAM(pc)              ' use the command byte as an index to get the main databse index
                iIndex = OpCodeIndex(iCode)  ' get the meta data we need based on the command byte.
                sCode = OpCodeDB(iIndex).opName ' get the name of the opcode. i.e. LDA, INC, etc.
                RunCodeParamData.Code = iCode
                ' let's now get some data for the command - based of of the command code's addressing mode
                If Not GetCmdCodeParams(iIndex) Then Exit Sub
                ' reset the PCnoInc variable. This being false will cause the PC to increment automatically after each instruction, based off of addressing mode.
                ' if an instruction itself adjusts the PC - such as JSR, JMP, BRA, etc. - then it will set PCnoInc to true to stop further changes to the PC
                Status.PCnoInc = False
                With RunCodeParamData
                    ' PageBoundaryCross is used to indicate if whatever an instruction has done has caused it to go beyond the current page. I.e. an instruction
                    ' is at &0E00 but jumps or pulls data from &DF0 - instruction i page &0E and data in &0D. This might result in this being set depending on addressing mode
                    ' and the particular instruction. Please review the addressing modes in Module1 FUnction GetCmdCodeParams(ByVal iIndex As Byte) As 
                    .PageBoundaryCrossed = False
                    Select Case iCode
                        ' BRK
                        Case &H0 ' BRK  
                            ' In the 65C02, the order of pushes following an interrupt Is PC high, PC low And SR. Following the stack activity,
                            ' the I - bit Is set in SR And the PC Is loaded from $FFFE-$FFFF. Upon executing RTI, the 'C02 pulls SR, followed by PC low and then PC high.
                            If (Val(Status.PC) + 1) > &HFFFF Then
                                StackPushByte(0)        ' if the return address (PC+1) being pushed on to the stack exceeds the upper 
                                StackPushByte(0)        ' limit of the memory map then we need to push &0000 instead
                                StackPushByte((Status.PC + 1) \ 256)                     ' push MSB of PC+1 to stack
                                StackPushByte((Status.PC + 1) Mod 256)                   ' push LSB of PC+1 to stack
                            End If
                            ' set the B and D flags, although B is temporary - done only so that the copy pushed to the stack has it set. this is done
                            ' as only the stack version should have B set so that any ISR code can tell if an interrupt was set by h/w or by a BRK.
                            Status.SR.B = True ' set this as this indicates that the source of the interrupt is a BRK instruction
                            Status.SR.D = False ' clear this as this happens as park of the BRK instruction (65C02 only - NMOS 6502 does not do this)
                            sr = SR_ConvertSRtoByte()   ' convert the status register into a byte
                            StackPushByte(sr)           ' push the status register as a byte on to the stack
                            Status.SR.B = False     ' set the break flag back to 0 now that we've pushed the staus register on to the stack
                            Status.SR.I = True      ' as this is an interrupt set the interrupt flag
                            Status.PC = MakeWord(RAM(&HFFFE), RAM(&HFFFF))  ' set the PC to the contents of the IRQ vector at &FFFE-&FFFF
                            Status.PCnoInc = True                           ' don't increment the PC automatically afterwards
                            Status.Cycles = 7
                        ' BIT
                        Case &H89 ' BIT #00 [Immediate]
                            Status.Cycles = 2
    						. more 65C02 instructions
    						. here, each within it's 
    						. own 'case' statement
                    End Select
                    ' Update the running count of the 6502 cpu cycles used
                    Status.CyclesTotal = Status.CyclesTotal + Status.Cycles
                    ' Let's see if the user still wants to step through the 6502 code (if stepping enabled). Can also quit by cancelling
                    If Running.stepping Then msgButton = MsgBox("Command:   " & sCode & Chr(13) &
                           "opCode = &" & Hex(iCode).PadLeft(2, "0") & Chr(13) &
                           "PC = &" & Hex(Status.PC).PadLeft(4, "0") & Chr(13) &
                           "A = &" & Hex(Status.A).PadLeft(2, "0") &
                           ",  X = &" & Hex(Status.X).PadLeft(2, "0") &
                           ",  Y = &" & Hex(Status.Y).PadLeft(2, "0") & Chr(13) &
                           "SP = &01" & Hex(Status.SP).PadLeft(2, "0") & Chr(13) &
                            "Flags [NVxBDIZC <- bit 0] = " & Mid(Str(Val(Status.SR.N)), 2) & Mid(Str(Val(Status.SR.V)), 2) & "x" & Mid(Str(Val(Status.SR.B)), 2) & Mid(Str(Val(Status.SR.D)), 2) &
                            Mid(Str(Val(Status.SR.I)), 2) & Mid(Str(Val(Status.SR.Z)), 2) & Mid(Str(Val(Status.SR.C)), 2) & Chr(13) &
    						"Cycles = " & Mid(Str(Status.Cycles), 2) & Chr(13) &
                           "Total Cycles = " & Mid(Str(Status.CyclesTotal), 2) & Chr(13) & Chr(13) &
                           "[Yes] to keep stepping" & Chr(13) & "[No] to run without stepping" & Chr(13) & "[Cancel] to quit running the code", MsgBoxStyle.YesNoCancel, "INFO")
                    If msgButton = vbCancel Then Running.Status = False
                    If msgButton = vbNo Then Running.stepping = False
                    ' increment the program counter. the value to increment by is set by the instruction which has just run.
                    ' in the case of instructions which actually set the program counter then status.PCnoInc is set to true which results in no addition to the program counter (PC).
                    If Not Status.PCnoInc Then
                        If Val(Status.PC + .NumDataBytes + 1) > &HFFFF Then
                            Status.PC = &H10000 - Val(Status.PC + .NumDataBytes + 1)    ' if the program counter is > &FFFF (65535) then loop to address &0000+
                            Status.PC = Status.PC + .NumDataBytes + 1 					' otherwise increse the PC by the number of bytes
                        End If
                    End If
                End With
                ' ensure that other tasks can run
                ' keep looping as long as the code still needs to
            Loop While Running.Status

  5. #5
    Super Moderator Shaggy Hiker's Avatar
    Join Date
    Aug 2002

    Re: App gets too busy for user input (VB2015)

    DoEvents is certainly controversial. Some folks hate it, others find it mildly upsetting. I have no issue with it for certain tasks, but those tasks are in tiny programs, or test programs (which also tend to be tiny), where I don't really care whether there are any side effectts. One thing to note is that DoEvents is SLOW. This won't be noticeable if you are calling it one or two times second, but if the loop is fast without DoEvents, then it will be noticeably slower WITH DoEvents. Whether or not that matters is up to you.

    The other issue with DoEvents is it does a kind of magic. Normally, if you understand how Windows works, the course of action is reasonably predictable. Events come in at a reasonable pace in a reasonable order. Once you introduce DoEvents, you are telling the code to pause while ALL pending messages are processed, which can make things happen in unpredictable orders. There are times where that can cause trouble. On the other hand, there are times when it is by far the simplest solution to a problem. However, I think it is safe to say that ANY time where DoEvents is a solution, threading is ALSO a solution, and is often going to be superior in both performance and reliability, at the cost of being more complicated.
    My usual boring signature: Nothing

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts


Click Here to Expand Forum to Full Width