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)