Threaded Delphi Tasks In A Thread Pool By OmniThreadLibrary

The OTL developed by Primoz Gabrijelcic is a simple to use threading library for Delphi. The “is simple” means that Gabriel has successfully hidden from you the complexity of running tasks in threads where thread synchronization and critical sections for data protection are nicely handled by the OTL.

While OTL is easy to use, one needs to invest time to figure out all the possibilities OTL exposes as (mostly is the case) there’s no good documentation but lots and lots of examples to help you get to speed with OTL.

Enough theory, let’s see OTL in action: background tasks and thread pool implementation:

For the sake of simplicity the task I want to be executed in the background (a thread) is a simple Sleep procedure call – the easiest way to mimic some workload that would be done by the real-world task you would like to be executed in a separate thread. Sleep procedure is (from my point of view) so useless but so great at the same time: Sleep Sort in OTL.

The OTL thread pool implementation is based around the “GlobalOmniThreadPool” singleton instance of IOmniThreadPool. You can create your own IOmniThreadPool, but GlobalOmniThreadPool would be enough most of the time.

GlobalOmniThreadPool can be used (among lots of other things) to specify how many threads should process your tasks (the “MaxExecuting” property). Also, the CountExecuting and CountQueued would return the number of tasks (threads) being executed at a time and also the number of tasks waiting in the queue.

You could set all the required GlobalOmniThreadPool properties in the OnCreate event of your main form, for example:

procedure TMainOTLTestForm.FormCreate(Sender: TObject);
begin
  GlobalOmniThreadPool.MaxExecuting := 2 * System.CPUCount;
end;

The above sets the number of threads in the GlobalOmniThreadPool to be 2 * number of processors (cores) on a machine.

Here’s the real work using, what is called in OTL, “low level” approach:


procedure TMainOTLTestForm.Button1Click(Sender: TObject);
const
  MSG_START = 1;
var
  iTask, numTasks, delay_ms : integer;
begin
  numTasks := 100;

  delay_ms := Random(100);

  for iTask := 1 to numTasks do
  begin
    Application.ProcessMessages;

    CreateTask(
      procedure(const task: IOmniTask)
      var
        sleepTime : integer;
      begin
        task.Comm.Send(MSG_START);

        sleepTime := task.Param['SleepTime'].AsInteger;

        //some task workload - this is where your background processing goes
 Sleep(sleepTime);
      end)
      .OnMessage(
        procedure(const task: IOmniTaskControl; const msg: TOmniMessage)
        var
          taskNumber : integer;
        begin
          if msg.MsgID = MSG_START then
          begin
            taskNumber := task.Param['TaskNumber'].AsInteger;

            LogPoolStatus(Format('task %d / %d start', [taskNumber, task.UniqueID]));
          end;
        end)
      .OnTerminated(
        procedure(const task: IOmniTaskControl)
        begin
          LogPoolStatus(Format('task id %d terminated', [task.UniqueID]));
        end)
      .SetParameter('TaskNumber', iTask)
      .SetParameter('SleepTime', delay_ms)
      .Unobserved
      .Schedule;
  end; (* for loop *)

  //wait all finished
  while GlobalOmniThreadPool.CountExecuting + GlobalOmniThreadPool.CountQueued > 0 do
    Application.ProcessMessages;

  //all task completed
  LogPoolStatus('ALL DONE');
end;

Let’s first deal with the less complex sections of the above example: the LogPoolStatus is a form (main UI) level procedure used to print out (add a line in a Memo control) the status of the thread pool and the task being executed.

procedure TMainOTLTestForm.LogPoolStatus(const msg : string);
begin
  Memo1.Lines.Add(Format('MSG: %s. Pool: %d executing / %d queued',
    [msg, GlobalOmniThreadPool.CountExecuting, GlobalOmniThreadPool.CountQueued]));
end;

Here’s what happens in the above code:

  1. 100 (numTasks variable) tasks should be executed out of the main thread (in the background).
  2. For each task we create an IOmniTask(Control) instance using the CreateTask procedure.
  3. The CreateTask uses an anonymous procedure to specify what the background task should be.
  4. The task we execute in the background is a simple Sleep call.
  5. When the tasks starts (is grabbed by the pool to start its execution in a background thread) we send a message using task.Comm.Send that will be handled by the OnMessage procedure.
  6. The OnMessage is executed in the main thread – this is where you can safely update your user interface. Think of OnMessage as the Synhronize procedure as implemented in Delphi’s TThread.
  7. When the task is done executing (Sleep function has finished) the OnTerminated can be used to again safely update the main UI.
  8. The task would certainly need some input parameters – this is what SetParameter can be used for. We send to the background task two parameters: SleepTime and TaskNumber.
  9. The Unobserved tells to the poll that we are not using TOmniEventMonitor instance to control freeing of the created task, rather the task should free itself when terminated.
  10. Finally, Schedule adds the task to the thread pool.

Note: there are different approaches in OTL to do the same. I love anonymous methods and how all the code (the task) to be executed in a separate thread is at the same place.

In my real-world threaded task I needed to allow the user to either wait for all the tasks to be competed or cancel the tasks that are waiting in the pool.

To wait for all the task to be finished:

while GlobalOmniThreadPool.CountExecuting + GlobalOmniThreadPool.CountQueued > 0 do
  Application.ProcessMessages;

To cancel the tasks in the pool queue:

GlobalOmniThreadPool.CancelAll;

this articles is By Zarko Gajic

You can skip to the end and leave a response. Pinging is currently not allowed.

Leave a Reply

*