Delphi OmniThreadLibrary and Async

1.8k Views Asked by At

Hopefully a simple one. I am using an OTL Parallel.For loop to process lots of data. The amount of data can change and if there is a lot (that takes over 2 seconds) Windows flickers the application form and gives a temporary "not responding" status in the title bar.

To get around this I thought I could put the procedure with the Parallel.For loop inside an OTL Async call, like

done:=false;
Async(ProcedureThatDoesParallelFor).Await(
procedure begin
done:=true;
end);
repeat application.processmessages until done=true;

This works (or seems to work) but can lead to the program just aborting/exiting without any error messages. It only seems to cause the silent abort problem when the Parallel.For loop is very quick to run.

If I remark the above code and take the call to ProcedureThatDoesParallelFor outside of it the app runs fine without unexpected quitting, so I am assuming it must be the Async call causing the problem. Or a combination of Parallel.For within Async?

Is using Async the best way to run another procedure and wait for it to finish? Is there a better OTL way of doing this?

Thanks for any ideas or solutions.

Here is the simplest example to show the crashing error. Single form with a memo and button. Click the button and the program will hang around iteration 300.

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls,OtlParallel,OtlTaskControl;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure AsyncParallelFor;
var iterations:integer;
    blah:integer;
begin
     iterations:=10;
     //for iter:=0 to limit-1 do
     Parallel.For(0,iterations-1).Execute(procedure(iter:integer)
     var x,y:integer;
     begin
          for y:=0 to 50 do
          begin
               for x:=0 to 50 do
               begin
                    blah:=x+y;
               end;
          end;
     end);
end;

procedure AsyncProcedure;
var done:boolean;
begin
     done:=false;
     Parallel.Async(
          procedure
          begin
               //executed in background thread
               AsyncParallelFor;
          end,
          Parallel.TaskConfig.OnTerminated(
          procedure (const task: IOmniTaskControl)
          begin
               //executed in main thread after the async has finished
               done:=true;
          end
          )
     );
     //this point is reached immediately after the call to Async
     //the repeat loop waits until the Async is finished being signalled via done variable
     repeat
           application.processmessages;
     until done=true;
end;


procedure TForm1.Button1Click(Sender: TObject);
var iters:integer;
begin
     iters:=0;
     repeat
           memo1.lines.add('Iteration '+inttostr(iters)+'...');
           memo1.lines.add('Before Async');
           application.processmessages;
           AsyncProcedure;
           memo1.lines.add('After Async');
           application.processmessages;
           inc(iters);
     until 1>2;


end;

end.

AsyncParallelFor shows the basic nested loops. Just a simple addition in there to demo the issue.

AsyncProcedure does the OTL Async call and waits for the return.

I have a lot of non parallel code before and after the call to AsyncProcedure that need to wait for the parallel.for loop to finish.

If I change the button click to call AsynParallelFor directly without the Async then there is no hang.

1

There are 1 best solutions below

4
LU RD On

In your AsyncProcedure, there is no need to repeatedly wait for the async call to finish. This defeats the event driven model that the OS is built on. Specially calling Application.ProcessMessages can lead to unexpected things to happen.

Use the OnTerminate event to signal that the async call is done and there take actions what to do next. In the example provided in this answer, a callback method is used to handle that.

A button click method is supposed to do only a short task, not an eternal loop with the dreaded calls to Application.ProcessMessages.

Instead, use a flag to indicate whether a new call to the async procedure should be done.


Below is an example how to modify your test with a callback method and an event driven model (I did not try the OTL calls, but I would be surprised if the library is the cause of your problems):

type
  TForm1 = class(TForm)
    BtnStart: TButton;
    BtnStop: TButton;
    Memo1: TMemo;
    procedure BtnStartClick(Sender: TObject);
    procedure BtnStopClick(Sender: TObject);
  private
  { Private declarations }
    fDoRepeat : Boolean;
    fIterations : Integer;
    procedure MyCallbackMethod(Sender : TObject);
  public
  { Public declarations }
  end;

procedure AsyncProcedure( MyCallbackMethod : TNotifyEvent);
begin
  Parallel.Async(
    procedure
    begin
      //executed in background thread
      AsyncParallelFor;
    end,
  Parallel.TaskConfig.OnTerminated(
    procedure (const task: IOmniTaskControl)
    begin
      //executed in main thread after the async has finished
      MyCallbackMethod(Nil);
    end)
  );
end;

procedure TForm1.MyCallbackMethod(Sender : TObject);
begin
  if (Sender = nil) then // Callback from AsyncProcedure
     memo1.lines.add('After Async');
  if fDoRepeat then begin
    Inc(fIterations);
    memo1.lines.add('Iteration '+inttostr(fIterations)+'...');
    memo1.lines.add('Before Async');
    AsyncProcedure(MyCallbackMethod);        
  end;
end;

procedure TForm1.BtnStartClick(Sender: TObject);
begin
  fDoRepeat := true;
  fIterations := 0;
  BtnStart.Enabled := false;
  MyCallbackMethod(Sender);  // Start iteration event looping
end;

procedure TForm1.BtnStopClick(Sender: TObject);
begin
  fDoRepeat := false;  // Stop iteration loop
  BtnStart.Enabled := true;
end;

Update

Running the above test in debug mode gave:

Out of memory

after 387 iterations in an OTL unit allocating memory for a buffer (and it is running slow).

Testing the OTL Parallel.For() with some other examples from Updating a Progress Bar From a Parallel For Loop (Plus Two Bonuses) did not improve the outcome. Program hangs at 400 iterations.


Using the bug ridden Delphi PPL did in fact work, though.

Uses
  Threading;

procedure AsyncParallelFor;
var
  iterations:integer;
  blah:integer;
begin
  iterations := 10;
  TParallel.For(0,iterations-1,
    procedure(iter : integer)
    var x,y:integer;
    begin
      for y := 0 to 50 do
      begin
        for x := 0 to 50 do
        begin
          blah := x+y;
        end;
      end;
    end);
end;

procedure AsyncProcedure( MyCallbackMethod : TNotifyEvent);
begin
  TTask.Run(
    procedure
    begin
      AsyncParallelFor;
      //executed in main thread after the async has finished
      TThread.Queue(nil,
        procedure
        begin
          MyCallbackMethod(Nil);
        end
      );
  end);
end;

To update the GUI within a parallel for loop, just use this code within the loop:

TThread.Queue(nil,
  procedure 
  begin
    // Some code that updates the GUI or calls a method to do so.
  end
);