Progress bars and background tasks and thread control

DCL to CML changes under way

We've copied the DCL docs to this new location and are still in the process of scrubbing the pages and fixing all the text and links. If you notice anything amiss feel free to please post a comment at the bottom of the page. Thanks!

I recently wrote about a thread-safe class for displaying information from background processes. The class worked fine, but it exercised absolutely no control over the thread itself. That is, in the accompanying app it was possible to launch multiple instances of the same worker thread, with amusing if not disastrous results, and it was also possible to close the test program before all background tasks had completed, with disastrous and not at all amusing results.

What I needed was a class to remedy these deficiencies.

I’ve covered this ground before – in Classes for Background Processes in Clarion Magazine I introduced a class for managing background threads. It’s a tricky business mixing classes and threading because you can’t Start() a method (which would be handy); you can only Start() a procedure. 

There are two kinds of code to consider when running background processes: 1) the code that executes once for each time through the processing loop, and 2) the thread control code, for tasks such as knowing when the thread is running and pausing or closing the thread on demand.

Short of using global variables (not generally a good idea) my standard approach is to put the thread management code in a class, and then pass a class instance to the Start()ed procedure. I explained how to do that in the previous article.

But once you have the class instance in the Start()ed procedure, there are at least two different ways to handle the code. One, which is the approach I used in the ClarionMag article, is to create a DoTask method that sits inside a loop. To use DoTask you need to derive the method and load it up with whatever code should execute each time through the loop.

In my recent background task progress bars example I went for a marginally simpler approach: the Start()ed task procedure looks like any other process but includes a bit of code before, after, and inside the loop to handle the control aspect. I still need to derive a virtual method, but in this case it’s the method that does the actual Start()ing.

Adding thread control

My first challenge was to modify my original threading class to handle starting and stopping a single instance of a Start()ed procedure.

Here’s what the demo procedure looks like:

                                            MEMBER('ThreadDemo')

                                            MAP
                                            END

ThreadDemo_Main                             procedure

Window                                          WINDOW('Background thread demo'),AT(,,223,101),GRAY,FONT('Microsoft Sans ' & |
                                                    'Serif',8),TIMER(1)
                                                    BUTTON('Stop Task'),AT(114,26,70),USE(?StopTask)
                                                    BUTTON('Start Task'),AT(28,26,70,14),USE(?StartTask)
                                                    STRING('Background process is not running'),AT(28,51),USE(?status)
                                                END

Task                                            class(DCL_System_Threading_Thread)
StartWorkerProcedure                                procedure,derived
                                                end
    CODE
    open(window)
    accept
        case event()
        of EVENT:CloseWindow
            if not Task.Stopped()
                message('Cannot exit while the background Task is running!')
                cycle
            end
        of EVENT:Timer
            ?status{prop:text} = choose(Task.Stopped(),'Background process is not running','Background process is running')
        end
        case accepted()
        of ?StopTask
            Task.Stop()
        of ?StartTask
            Task.Start()
        end
    end
    close(window)
    
Task.StartWorkerProcedure                   procedure!,derived
    code
    start(ThreadDemo_BackgroundProcess,25000,address(self))
    

Note that I’ve derived the StartWorkerProcedure method which calls my actual background process. This can be any procedure that executes code in a loop, such as a report or a process.

Here’s the ThreadDemo_BackgroundProcess procedure:

 MEMBER('ThreadDemo')

                                            MAP
                                            END

ThreadDemo_BackgroundProcess                procedure(string address)


Thread                                          &DCL_System_Threading_Thread
sleeptime                                       long
loopcount                                       long

    CODE
 Thread &= (address)
    if Thread &= NULL
        return
    end
    Thread.WorkerProcedureHasStarted()
    ! Implement your own loop structure as you see fit, but be sure to include the code
    ! to terminate the thread as shown in this loop. Your loop will probably not have a fixed
    ! number of iterations - this one does simply so it doesn't go on forever. 
    loop 10 times 
        if Thread.StopRequested() then break.
        ! Do something here. This is a demo procedure so all it does is sleep.
        sleep(500)
    end
    Thread.WorkerProcedureHasEnded()    

The procedure's code is almost all thread control code because it is, after all, just a demo.

The DCL_System_Thread is class is really pretty simple. For the most part it revolves around two flags, Running and StopRequested.  Here’s the class header:

DCL_System_Threading_Thread                 Class,Type,Module('DCL_System_Threading_Thread.CLW'),Link('DCL_System_Threading_Thread.CLW',_DCL_Classes_LinkMode_),Dll(_DCL_Classes_DllMode_)
Running                                         byte,private
StopRequest                                     byte,private
Construct                                       Procedure()
Destruct                                        Procedure()
Start                                           procedure
StartWorkerProcedure                            procedure,virtual
Stop                                            procedure
Stopped                                         procedure,bool
StopRequested                                   procedure,bool
WorkerProcedureHasEnded                         procedure
WorkerProcedureHasStarted                       procedure
                                            End

 

And here’s the class code:

DCL_System_Threading_Thread.Construct   Procedure()
    code
    self.StopRequest = false
    self.Running = false

DCL_System_Threading_Thread.Destruct    Procedure()
    code
    
DCL_System_Threading_Thread.Start       procedure
    code
    if ~self.running
        self.StartWorkerProcedure()
    end
DCL_System_Threading_Thread.StartWorkerProcedure        procedure!,virtual
    code
    ! To use this class you will typically create a derived class somewhere in your
    ! application code, probably in the procedure that launches your worker
    ! procedure.
    
DCL_System_Threading_Thread.Stop        procedure
    code
    self.StopRequest = true

DCL_System_Threading_Thread.Stopped     procedure!,bool
    code
    return choose(self.running=false,true,false)
    
DCL_System_Threading_Thread.StopRequested       procedure!,bool
    code
    return self.StopRequest

DCL_System_Threading_Thread.WorkerProcedureHasEnded     procedure
    code
    self.Running = FALSE
    self.StopRequest = false
    
DCL_System_Threading_Thread.WorkerProcedureHasStarted   procedure
    code
    self.running = true
    self.StopRequest = false

Note that the Start() method calls the StartWorkerProcedure method, but only if the thread isn’t already running. And StartWorkerProcedure is the one you derive so you can call your own process. You never call StartWorkerProcedure directly – it’s always called automatically by the base class’s Start() method.

Here’s the demo app in action:

You can click on Start Task as many times as you like, but the Start() only executes if there is no existing running process. Clicking Stop Task will cause the process to terminate on the next loop.

If you try to exit the program while the task is running, you’ll see this message:

Everything works just fine. But there’s no indication of the task’s background process yet.

It’s conceivable that a background task will need to run without updating a UI somewhere, so I don’t necessarily want to add UI capabilities to this class. Instead I created a derived class called DCL_System_Threading_DisplayedThread. Here’s the header:

DCL_System_Threading_DisplayedThread        Class(DCL_System_Threading_Thread),Type,Module('DCL_System_Threading_DisplayedThread.CLW')|
                                                ,Link('DCL_System_Threading_DisplayedThread.CLW',_DCL_Classes_LinkMode_),Dll(_DCL_Classes_DllMode_)
UI                                              &DCL_UI_BackgroundProgressDisplay
Construct                                       Procedure()
Destruct                                        Procedure()
                                            End

And here’s the code:

DCL_System_Threading_DisplayedThread.Construct      Procedure()
    code
    self.UI &= new DCL_UI_BackgroundProgressDisplay
DCL_System_Threading_DisplayedThread.Destruct       Procedure()
    code
    dispose(self.UI)

As you can see, all this class does is create an instance of DCL_UI_BackgroundProgressDisplay, which I introduced last time.

I modified my two-task demo from the previous article, with the code as follows:

                                            MEMBER('ThreadDemo')
                                            MAP
                                            END
ThreadDemo_Main                             procedure
Window                                          WINDOW('Background task progress bars'),AT(,,434,104),GRAY,FONT('Microsof' & |
                                                    't Sans Serif',8)
                                                    BUTTON('Start task 1'),AT(15,19,51,19),USE(?StartTask1),ICON(ICON:None)
                                                    STRING('Message'),AT(148,18,253),USE(?Message1)
                                                    PROGRESS,AT(148,31,253,6),USE(?PROGRESS1),RANGE(0,100)
                                                    BUTTON('Start task 2'),AT(15,57,51,19),USE(?StartTask2),ICON(ICON:None)
                                                    STRING('Message'),AT(148,56,253,10),USE(?Message2)
                                                    PROGRESS,AT(148,70,253,6),USE(?PROGRESS2),RANGE(0,100)
                                                    BUTTON('Stop task 1'),AT(78,19,51,19),USE(?StopTask1),ICON(ICON:None)
                                                    BUTTON('Stop task 2'),AT(78,57,51,19),USE(?StopTask2),ICON(ICON:None)
                                                END
Task1                                           class(DCL_System_Threading_DisplayedThread)
StartWorkerProcedure                                procedure,derived
                                                end
Task2                                           class(DCL_System_Threading_DisplayedThread)
StartWorkerProcedure                                procedure,derived
                                                end
    CODE
    open(window)
    accept
        case event()
        of EVENT:OpenWindow
            task1.UI.Enable()
            task2.UI.Enable()
        of EVENT:CloseWindow
            if not Task1.Stopped() or not Task2.Stopped()
                message('Please wait until the background processes complete')
                cycle
            end
        end
        case accepted()
        of ?StartTask1
            Task1.Start()
        of ?StartTask2
            Task2.Start()
        of ?StopTask1
            Task1.Stop()
        of ?StopTask2
            Task2.Stop()
        end
    end
    close(window)
    
Task1.StartWorkerProcedure                  procedure!,derived
    code
    self.ui.SetProgressControlFEQ(?progress1)
    self.ui.SetStringControlFEQ(?Message1)
    start(ThreadDemo_BackgroundProcess,25000,address(self),100,100)
Task2.StartWorkerProcedure                  procedure!,derived
    code
    self.ui.SetProgressControlFEQ(?progress2)
    self.ui.SetStringControlFEQ(?Message2)
    start(ThreadDemo_BackgroundProcess,25000,address(self),100,100)

There are two additions here. First, the calls to Taskx.UI.Enable() in the OpenWindow event, and second the Taskx.UI.SetProgressControlFEQ and Taskx.UI.SetStringControlFEQ calls in the derived StartWorkerProcedure methods.

The background procedure now looks like this, with the addition of two method calls to set the progress bar and the progress message values.

ThreadDemo_BackgroundProcess            procedure(string address, string delay, string count)
Task                                        &DCL_System_Threading_DisplayedThread
sleeptime                                   long
loopcount                                   long
x                                           long
    CODE
    Task &= (address)
    if Task &= NULL
        return
    end
    Task.WorkerProcedureHasStarted()
    sleeptime = delay
    loopcount = count
    ! Implement your own loop structure as you see fit, but be sure to include the code
    ! to terminate the thread as shown in this loop. Your loop will probably not have a fixed
    ! number of iterations - this one does simply so it doesn't go on forever. 
    loop x = 1 to loopcount
        if Task.StopRequested() then break.
        ! Do something here. This is a demo procedure so all it does is sleep.
        task.UI.SetProgressControlValue(x)
        task.UI.SetStringControlValue(x / loopcount * 100 & ' percent done')        
        sleep(sleeptime)
    end
    Task.WorkerProcedureHasEnded()

Here’s the demo program in action. It functions just as the other demo did but it now updates the window with the progress status. Again, you can only run one instance of either task at once and you cannot exit the program until all tasks have completed.

 

As with the non-UI example, if you attempt to exit the program before the tasks finish you'll get an error message. In fact the background tasks continue to run while the error message is displayed, although the UI doesn't update because the Message() statement is blocking the Accept loop. As soon as you close the Message window the window is updated with the current task status. 

The only thing missing now is a template to make this really easy to use…