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…