Friday, October 16, 2015

Updating a Progress Bar From a Parallel For Loop (Plus Two Bonuses)

During the Q&A part of my Simplify Parallel Programming with Patterns presentation on CodeRage X, I’ve promised the listeners to publish a demo for updating a progress bar from a parallel for loop.

In this article I’ll try to explain few different approaches that all solve this problem. I’ve also put together a demo project which demonstrates all techniques.


1. Parallel Programming Library [XE7+, any platform]

1.a Windows Messages

First idea is to send a windows message with a ‘percent of processed items’ as a parameter to the main form and let it update itself. To do that, however, we must execute the parallel for in a background thread. TParallel.For is blocking in the PPL, which means that messages are not processed while the for is being executed. To fix that, we will execute it inside a task.

Next problem is how to determine ‘percent of processed items’. We know how many items will be processed (high boundlow bound + 1), but we must also somehow count how many items were already processed and we must do that in a way that is compatible with a multithreaded approach. The fragment below uses a TInterlocked record to atomically increment a shared variable – this way that variable will always contain a correct value.

At the end we just send the current percentage to the main thread. It is always a good idea not to overflow the system with messages so the demo program sends a progress update message only every ten processed items.

An alternative way would be to just send a message when percentage changes, but that is again hard to implement in a multithreaded world as we must then access current percentage and last percentage in a shared and atomic manner.

At the very end the code queues the AfterTest method to be executed in the main thread. This method re-enables the UI (which got disabled in BeforeTest) and cleans the FTask field. For details, see the demo code.

procedure TfrmUpdateProgressBar.btnPPLMessageClick(Sender: TObject);
var
handle:
THandle;
begin
BeforeTest;

// Local copy of a form handle. As I've said in the presentation -
// don't access the
UI from a thread! (Although in this case that will
// not cause any problems - but
it is always best to stay on the safe side.)

handle :=
frmUpdateProgressBar.Handle;

// While the parallel code is running, main form must process messages, so we
// have
to push a TParallel.For into background by wrapping it inside a task.

FTask :=
TTask.Run(
procedure
var
processed: integer;
// shared counter
total : integer;
// total number of items to be processed
begin
processed := 0
;
total := 1000
;
TParallel.For(1, 1000
,
procedure (i:
integer)
var
new:
integer;
begin
Sleep(10);
//do the work
new := TInterlocked.Increment(processed);
// increment the shared counter
if (new mod 10) = 0 then
// update the progress bar every 10 processed items
PostMessage(handle, WM_UPDATE_PROGRESS,
Round(new / total * 100), 0
);
end
);
// TParallel.For

// Update the UI
TThread.Queue(nil,
AfterTest);
end
);
// TTask.Run
end;

1. b TThread.Queue

This method is similar to the previous one, except that instead of sending a message we just update the UI from a queued method. Everything else stays the same.

procedure TfrmUpdateProgressBar.btnPPLWithQueueClick(Sender: TObject);
begin
BeforeTest;

// While the parallel code is running, main form must process messages, so we
// have
to push a TParallel.For into background by wrapping it inside a task.

FTask :=
TTask.Run(
procedure
var
processed: integer;
// shared counter
total : integer;
// total number of items to be processed
begin
processed := 0
;
total := 1000
;
TParallel.For(1, 1000
,
procedure (i:
integer)
var
new:
integer;
begin
Sleep(10);
//do the work
new := TInterlocked.Increment(processed);
// increment the shared counter
if (new mod 10) = 0 then
// update the progress bar every 10 processed items
TThread.Queue(nil
,
procedure
begin
//update the progress bar in the main thread
pbParallel.Position := Round(new / total * 100
);
end
);
//TThread.Queue
end
);
// TParallel.For

// Update the UI
TThread.Queue(nil,
AfterTest);
end
);
// TTask.Run
end;

1.c TTimer

The third approach decouples the worker from the main thread as much as possible. The only connection point is a shared variable (FProgress), which holds the current percentage of processed items. It is updated from the worker thread and is read from a timer on a form. This timer then updates the progress bar (see the code for details).


Even this kind of shared variable (one writer, one reader) can cause problems if not accessed atomically so again we are using TInterlocked to update and to read the variable. The ‘2.c’ example below shows a simpler way of accessing such shared variable – a TGp4AlignedInt record. You can use any approach (TInterlocked or TGp4AlignedInt) with any multithreading library (PPL, OTL, or anything else).

procedure TfrmUpdateProgressBar.btnPPLWithTimerClick(Sender: TObject);
begin
BeforeTest;
FProgress := 0
;
tmrUpdateProgress.Enabled :=
true;

// While the parallel code is running, main form must process messages, so we
// have
to push a TParallel.For into background by wrapping it inside a task.

FTask :=
TTask.Run(
procedure
var
processed: integer;
// shared counter
total : integer;
// total number of items to be processed
begin
processed := 0
;
total := 1000
;
TParallel.For(1, 1000
,
procedure (i:
integer)
var
new:
integer;
begin
Sleep(10);
//do the work
new := TInterlocked.Increment(processed);
// increment the shared counter
if (new mod 10) = 0 then
// update the progress bar every 10 processed items
// Even with one reader and one writer accessing an 'integer'
// is not atomic!
TInterlocked.Exchange(FProgress, Round(new / total * 100
));
end
);
// TParallel.For

// Disable the timer when everything is processed.
TThread.Queue(nil
,
procedure
begin
// We have no idea if timer proc 'saw' FProgress = 100 so let's force
// the last
update.
pbParallel.Position := 100
;
tmrUpdateProgress.Enabled :=
false;
AfterTest;
end
);
//TThread.Queue
end
);
// TTask.Run
end;

2. OmniThreadLibrary [2009+, Windows/VCL]


2.a Windows Messages

The code below uses OTL to create a parallel for loop and Windows messages to update the user interface. As we can see, there’s less code, because we can simply push the loop into a background (.NoWait) and because it is easy to specify the code that should run in the main thread after the loop terminates (.TaskConfig.OnTerminated). Instead of TInterlocked, a TGp4AlignedInt from the GpStuff unit is used to implement a shared variable. (Internally, both TInterlocked and TGp4AlignedInt function exactly the same).

procedure TfrmUpdateProgressBar.btnOTLWithMessageClick(Sender: TObject);
var
handle:
THandle;
begin
BeforeTest;

handle :=
frmUpdateProgressBar.Handle;

FParallelFor := Parallel.For(1, 1000
)
.NoWait
.TaskConfig(Parallel.TaskConfig.OnTerminated(AfterTest));

FParallelFor.Execute(
procedure (i:
integer)
var
new:
integer;
begin
Sleep(10);
//do the work
new := FProcessed.Increment;
// increment the shared counter
if new mod 10 = 0
then
PostMessage(handle, WM_UPDATE_PROGRESS, Round(new / 1000 * 100), 0
);
end
);
// Parallel.For
end;

2.b TThread.Queue

You are free to use TThread.Queue from inside the OmniThreadLibrary parallel patterns, so this approach would look like a combination of 1.b and 2.a and is left as an exercise for the reader.


2.c TTimer

This again is very similar to the 1.c approach, except that OTL functions (NoWait, TaskConfig) are used to simplify the code. Again, TGp4AlignedInt is used instead of TInterlocked.

procedure TfrmUpdateProgressBar.btnOTLWithTimerClick(Sender: TObject);
begin
BeforeTest;
FProcessed.Value := 0
;
FProcessedPct.Value := 0;
tmrUpdateProgress.Enabled :=
true;

FParallelFor := Parallel.For(1, 1000
)
.NoWait
.TaskConfig(Parallel.TaskConfig.OnTerminated(
procedure
begin
// Disable the timer when everything is processed.
pbParallel.Position := 100
;
tmrUpdateProgress.Enabled :=
false;
AfterTest;
end
));

FParallelFor.Execute(
procedure (i:
integer)
var
new:
integer;
begin
Sleep(10);
//do the work
new := FProcessed.Increment;
// increment the shared counter
if new mod 10 = 0
then
FProcessedPct.Value := Round(new / 1000 * 100
);
end
);
// Parallel.For
end;

Bonus 1: Async/Await


In the CodeRage session I’ve mentioned that it’s very simple to implement the Async/Await pattern with PPL. Here’s how:

procedure AsyncAwait(async, await: TProc);
begin
TTask.Run(
procedure
begin
async();

TThread.Queue(nil
,
procedure
begin
await();
task := nil
;
end
);
// TThread.Queue
end
);
//TTask.Run
end;

You can also do it with the standard TThread class:

procedure AsyncAwait(async, await: TProc);
begin
TThread.CreateAnonymousThread(
procedure
begin
async();

TThread.Queue(nil
,
procedure
begin
await();
end
);
// TThread.Queue
end
).Start;
end;

And that’s how you would use it:

AsyncAwait(
procedure
begin
Sleep(1000
);
end
,
procedure
begin
AfterTest;
//enable buttons
end);

The demo project contains an implementation and a demo for TTask and TThread Async/Await.


Bonus 2: CodeRage X Links


I have published links to the slides and code for my Simplify Parallel Programming with Patterns presentation on the Presentations page. I’ll add the link to the video as soon as it is available online.

1 comment:

  1. It's often fine to have writer thread using atomic increments, and the reader thread just reading the memory. A common way to do progress then is to have a GUI timer polling a shared counter. The writer threads increment atomically but the GUI thread can just read the variable directly.

    ReplyDelete