Progress bars and background tasks
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!
Ever since true threading showed up in version 6, Clarion has been able to spawn background tasks that don't open windows but just get some job done. That ability is both a blessing and a curse.
The benefits are are as obvious as the problems with the alternatives. If you have a long-running task and you don't start it on a separate thread, you block the user interface until the task finishes. And if you start the task on another thread and you open a window, then you have one more UI element cluttering up the workspace. Okay, you can always hide the window, but that's a bit of a kludge.
And what if you have multiple background processes running and you want to monitor the status of those processes in one place?
What you need is a way to update a UI element on one thread using code on another thread.
Below is a screen shot of the sample program I'll be discussing in this article. I've started two tasks on separate threads, each of which has now window and only a simple loop of 100 repetitions with a sleep statement so it all doesn't go by in a blink of an eye.
Cross-thread messaging
The example program makes use of a class called DCL_UI_BackgroundProgressDisplay, and which is now part of The DevRoadmaps Clarion Library (DCL).
I'm really not happy with the name I've given this class, for reasons that may become clearer a little later on. So if you're reading this article and the class is no longer in DCL, don't be surprised. But it shouldn't be hard to find its replacement.
The class makes use of two Clarion language features: the ability to safely mediate access to data between threads, and a built-in protocol for sending messages across threads.
The notification mechanism consists of two Clarion functions: Notify and Notification. From the help for Notify:
The NOTIFY statement is called on the sender side. It generates the EVENT:Notify event and places it at the front of the event queue of receiver's thread top window. Generally, the EVENT:Notify event is a special event that can transfer up to 2 additional parameters (thread and parameter) to the receiver.
Execution of the sender thread continues immediately. It does not wait for any response from the receiver.
NOTIFY and NOTIFICATION are a functional replacement for the SETTARGET(,thread) statement. They can also be used for safe transfer information between threads.
Similarly, Notification:
The NOTIFICATION function is used by a receiving thread. It receives the notification code, thread number, and parameter passed by the sending thread’s NOTIFY statement.
NOTIFICATION returns FALSE (0) if the EVENT() function returns an event other than EVENT:Notify. If EVENT:Notify is posted, NOTIFICATION returns TRUE. Because calls to NOTIFY and NOTIFICATION are asynchronous, the sender thread can be closed already when receiver thread accepts the EVENT:Notify event.
So my background threads can use Notify to tell the UI thread it needs to redisplay some progress information. And I can update that progress information via an object I share between the UI and the background threads.
Here's the code for my Main UI procedure:
Main procedure Window WINDOW('Background task progress bars'),AT(,,364,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(86,19,253),USE(?Message1) PROGRESS,AT(86,32,253,6),USE(?PROGRESS1),RANGE(0,100) BUTTON('Start task 2'),AT(15,57,51,19),USE(?StartTask2),ICON(ICON:None) STRING('Message'),AT(86,57,253,10),USE(?Message2) PROGRESS,AT(86,71,253,6),USE(?PROGRESS2),RANGE(0,100) END ProgressDisplay1 DCL_UI_BackgroundProgressDisplay ProgressDisplay2 DCL_UI_BackgroundProgressDisplay code open(window) ProgressDisplay1.SetProgressControlFEQ(?progress1) ProgressDisplay1.SetStringControlFEQ(?Message1) ProgressDisplay2.SetProgressControlFEQ(?progress2) ProgressDisplay2.SetStringControlFEQ(?Message2) accept case event() of EVENT:OpenWindow ProgressDisplay1.Enable() ProgressDisplay2.Enable() end case accepted() of ?StartTask1 start(Task,,address(ProgressDisplay1)) of ?StartTask2 start(Task,,address(ProgressDisplay2)) end end close(window)
I have two instances of the DCL_UI_BackgroundProgressDisplay class. Each one is initialized with the field equate of the controls they are to update. When I start the Task procedure on its own thread, I pass the address of the appropriate class instance to the thread.
There's also an important Enable method call which I'll come back to shortly.
Here's the code for the Task procedure:
Task procedure(string ProgressObjectAddress) ProgressDisplay &DCL_UI_BackgroundProgressDisplay x long code ProgressDisplay &= (ProgressObjectAddress) loop x =1 to 100 sleep(50) ProgressDisplay.SetProgressControlValue(x) ProgressDisplay.SetStringControlValue(x & ' percent done') end
Task declares a reference to a DCL_UI_BackgroundProgressDisplay object. The following line of code assigns the reference to the passed address:
ProgressDisplay &= (ProgressObjectAddress)
Encosing ProgressObjectAddress (which is actually a string) in parentheses is a trick that causes the passed address to be evaluated, so the &= assignment works properly. Without the parentheses you'll get an "Illegal reference assignment or equivalence" error.
Other than that, the Task procedure's code is unremarkable. It simply contains two method calls, one to set the string control's value and another to set the progress control's value. Neither of these methods updates the control directly, however.
The DCL_UI_BackgroundProgressDisplay class
Here's the class declaration:
DCL_UI_BackgroundProgressDisplay Class,Type,Module('DCL_UI_BackgroundProgressDisplay.CLW'),Link('DCL_UI_BackgroundProgressDisplay.CLW',_DCL_Classes_LinkMode_),Dll(_DCL_Classes_DllMode_) CriticalSection &DCL_System_Threading_CriticalSection NotifyCode long NotifyEvent long ProgressControlFEQ long ProgressControlValue long StringControlFEQ long StringControlValue &string,private UIThread long,private !Errors &DCL_System_ErrorManager Construct Procedure() Destruct Procedure() DisposeStringControlText procedure,private Enable procedure SetStringControlFEQ procedure(long StringControlFEQ) SetStringControlValue procedure(string StringControlValue) SetProgressControlFEQ procedure(long ProgressBarControlFEQ,<long rangeLow>,<long rangeHigh>) SetProgressControlValue procedure(long ProgressControlValue) TakeEvent procedure End
There are a number of properties for tracking the string and progress controls, and for setting those values. The string control FEQ doesn't really need a setter method, but the progress control FEQ may as it can optionally take the low and high range parameters:
DCL_UI_BackgroundProgressDisplay.SetProgressControlFEQ procedure(long ProgressBarControlFEQ,<long rangeLow>,<long rangeHigh>) code self.ProgressControlFEQ = progressBarControlFEQ if not omitted(rangeLow) self.ProgressControlFEQ{prop:rangelow} = rangeLow end if not omitted(rangeHigh) self.ProgressControlFEQ{prop:rangeHigh} = rangeHigh end
To keep things consistent, I added a corresponding method for setting the string control FEQ.
I chose to use a string reference to store the string control's contents (as I have no way of knowing how large a string someone might actually want to pass), so the SetStringControlValue has a bit of work to do on each assignment:
DCL_UI_BackgroundProgressDisplay.SetStringControlValue procedure(string StringControlValue) code self.CriticalSection.Wait() self.DisposeStringControlText() self.StringControlValue &= new string(len(clip(StringControlValue))) self.StringControlValue = StringControlValue notify(self.notifyCode,self.UIthread) self.CriticalSection.Release()
Note also the belt-and-suspenders approach incorporating a critical section around the data that's being shared between the background thread and the UI thread.
Setting a progress control's value is marginally simpler in that no string resource is (re)created.
You can see the calls to Notify. But how are notifications received?
Remember the Enable method? Here's the code:
DCL_UI_BackgroundProgressDisplay.Enable procedure code self.UIThread = thread() register(event:notify,address(self.TakeEvent),address(self))
First, the UI thread number is saved so it can be used later (in the background thread) as a parameter to Notify.
Second, the class registers its own TakeEvent method as an event handler for notifications. That means that I don't need to insert any code into my UI thread's accept loop; TakeEvent will be called automatically on any Event:Notify.
Here's the TakeEvent code:
DCL_UI_BackgroundProgressDisplay.TakeEvent procedure code if NOTIFICATION(self.notifyCode) self.CriticalSection.Wait() if self.ProgressControlFEQ <> 0 self.ProgressControlFEQ{Prop:Progress} = self.ProgressControlValue display(self.ProgressControlFEQ) end if self.StringControlFEQ <> 0 self.StringControlFEQ{prop:text} = self.StringControlValue display(self.StringControlFEQ) end self.CriticalSection.Release() end
The core concept of the class is pretty simple:
- The SetProgressControlValue and SetStringControlValue calls happen on the background thread. They save values and send notifications to the UI thread.
- TakeEvent executes on the UI thread and receives notifications and displays those values in the UI.
Problems and other funky issues
This class has its issues, however. First, if you click either button before its task is finished, a second task starts and also updates the progress and string controls, with much flickering ensuing. On the bright side this shows the robustness of the class - it doesn't break when used by more than one background thread. But really this kind of behavior should be controllable.
The second problem is more critical. If you press Esc while a thread is executing you'll get a GPF, because the window has closed and its class instances have gone away but the background thread is still running, now with null references. Again, this should be controllable. And wouldn't it be nice to have a way to cancel a background process?
Here's the problem with the class as currently named. It really needs to do more than just display UI information. But those other things aren't described by the current class name, which focuses on the UI aspect of background thread behavior.
One class that does both isn't a great idea either, as there may be background threads that don't need to display anything to a UI on another thread. And really one class should only have one job, as described by the Single Responsibility Principle.
I'm mulling over the options; look for an update soon.
Download the sourceUINotifierDemo.zip
Get the DCL_UI_BackgroundProgressDisplay class from The DevRoadmaps Clarion Library (DCL)