A deceptively simple question – how do you update a progress bar from a ForEach loop – popped up on the Google+ OmniThreadLibrary community. The implementation turned out to be quite tricky so this is an explaining example created by Primož Gabrijelčič (55_ForEachProgress) which is now part of the OmniThreadLibrary SVN repository.
The starting point was a simple Parallel.ForEach loop which he further simplified in the demo.
We cannot simply update the progress bar from the ForEach executor as that code executes in a background thread and one must never ever access VCL GUI from a background thread! It is also no good to send “please update” Windows messages to main thread as Parallel.ForEach is by default blocking – it waits for all workers to stop working – and messages won’t be processed during ForEach execution.
First part of solution is to make ForEach non-blocking. To do that, we just add a .NoWait modifier. We also have to store the interface returned from Parallel.ForEach call into some global field or ForEach object will be destroyed on the exit from the current method (i.e. the method in which Parallel.ForEach is called).
The problem now is how to destroy the FWorker interface. Parallel.ForEach provides an OnStop delegate which is called when the last worker thread finishes its job. The delegate is, however, called from the worker thread so we must not destroy FWorker in there. That would cause the ForEach object to be destroyed while the last worker is still running and would lead to a crash or a hanged program. A correct way is to schedule the cleanup to the main thread by using the Invoke method.
Just a side note – I oh so miss type inference and better anonymous method syntax in Delphi! In Smart, OnStop handler would be written as
Because the values of i are not passed in order to the worker method, we cannot use them to determine the progress. Instead, the main form keeps its own count of work to be done. It is initialized before the Parallel.ForEach is created.
In the end, IncrementProgressBar, well, increments the progress bar. It also makes sure that we don’t overflow the Windows control with messages.
If you are enumerating over a very large range, you’ll also want to reduce number of Invoke(IncrementProgressBar) calls. Each Invoke causes a Windows message to be sent and sending millions of messages will negatively affect the program performance. The simplest way to do that is to only call IncrementProgressBar if the loop counter is a nice rounded value, for example:
This is just for self learning and self archives
All credits go to the original creator.
Source: originally written by Primož Gabrijelčič here
The starting point was a simple Parallel.ForEach loop which he further simplified in the demo.
Parallel .ForEach(1, CNumLoop) .Execute( procedure (const task: IOmniTask; const i: integer) begin // do some work Sleep(1); // update the progress bar - how? end );
We cannot simply update the progress bar from the ForEach executor as that code executes in a background thread and one must never ever access VCL GUI from a background thread! It is also no good to send “please update” Windows messages to main thread as Parallel.ForEach is by default blocking – it waits for all workers to stop working – and messages won’t be processed during ForEach execution.
First part of solution is to make ForEach non-blocking. To do that, we just add a .NoWait modifier. We also have to store the interface returned from Parallel.ForEach call into some global field or ForEach object will be destroyed on the exit from the current method (i.e. the method in which Parallel.ForEach is called).
type TfrmForEachWithProgressBar = class(TForm) … private FWorker: IOmniParallelLoop< integer>; end; FWorker := Parallel .ForEach(1, CNumLoop) .NoWait;
The problem now is how to destroy the FWorker interface. Parallel.ForEach provides an OnStop delegate which is called when the last worker thread finishes its job. The delegate is, however, called from the worker thread so we must not destroy FWorker in there. That would cause the ForEach object to be destroyed while the last worker is still running and would lead to a crash or a hanged program. A correct way is to schedule the cleanup to the main thread by using the Invoke method.
// reference must be kept in a global field so that the task controller // is not destroyed before the processing ends FWorker := Parallel .ForEach(1, CNumLoop) .NoWait // important, otherwise message loop will be blocked while // ForEach waits for all tasks to terminate .OnStop( procedure (const task: IOmniTask) begin // because of NoWait, OnStop delegate is invoked from the worker code; // we must not destroy the worker at that point or the program will // block or crash task.Invoke( procedure begin FWorker := nil; end ); end );
Just a side note – I oh so miss type inference and better anonymous method syntax in Delphi! In Smart, OnStop handler would be written as
.OnStop( lambda(task) task.Invoke(lambda FWorker := nil; end); end);Destruction being taken care of, we still have to update the progress bar. To do that, worker calls IncrementProgressBar method via the Invoke mechanism (so that it is executed in the main thread and can update the VCL).
FWorker.Execute( procedure (const task: IOmniTask; const i: integer) begin // do some work Sleep(1); // update the progress bar // we cannot use 'i' for progress as it does not increase sequentially // IncrementProgressBar uses internal counter to follow the progress task.Invoke(IncrementProgressBar); end );
Because the values of i are not passed in order to the worker method, we cannot use them to determine the progress. Instead, the main form keeps its own count of work to be done. It is initialized before the Parallel.ForEach is created.
pbForEach.Max := 100; pbForEach.Position := 0; pbForEach.Update; FProgress := 0; FPosition := 0;
In the end, IncrementProgressBar, well, increments the progress bar. It also makes sure that we don’t overflow the Windows control with messages.
procedure TfrmForEachWithProgressBar.IncrementProgressBar; var newPosition: integer; begin Inc(FProgress); newPosition := Trunc((FProgress / CNumLoop)*pbForEach.Max); // make sure we don't overflow TProgressBar with messages if newPosition <> FPosition then begin pbForEach.Position := newPosition; FPosition := newPosition; end; end;
If you are enumerating over a very large range, you’ll also want to reduce number of Invoke(IncrementProgressBar) calls. Each Invoke causes a Windows message to be sent and sending millions of messages will negatively affect the program performance. The simplest way to do that is to only call IncrementProgressBar if the loop counter is a nice rounded value, for example:
if (i mod 1000) = 0 then task.Invoke(IncrementProgressBar);
This is just for self learning and self archives
All credits go to the original creator.
Source: originally written by Primož Gabrijelčič here