Progress bars and background tasks and thread control (DCL)
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…


