Search code examples
delphiomnithreadlibrary

Exceptionhandling with IOmniParallelTask not working


Unhandled exceptions within IOmniParallelTask execution should (as I understand the docs) be caught by the OTL and be attached to IOmniTaskControl instance, which may be accessed by the termination handler from IOmniTaskConfig.

So after setting up the IOmniParallelTask instance with a termination handler like this:

fTask := Parallel.ParallelTask.NoWait.NumTasks(1);
fTask.OnStop(HandleOnTaskStop);

fTask.TaskConfig(Parallel.TaskConfig.OnTerminated(HandleOnTaskThreadTerminated));
fTask.Execute(TaskToExecute);

any unhandled exceptions within TaskToExecute:

procedure TFormMain.TaskToExecute;
begin
  Winapi.Windows.Sleep(2000);
  raise Exception.Create('async operation exeption');
end;

should be attached to the IOmniTaskControl instance you get within the termination handler:

procedure TFormMain.HandleOnTaskThreadTerminated(const task: IOmniTaskControl);
begin
  if not Assigned(task.FatalException) then
    Exit;

  memo.Lines.Add('an exception occured: ' + task.FatalException.Message);
end;

The issue at this point is, that the exception is not assigned to IOmniTaskControl.FatalException and I have no clue why.

Maybe some of you guys have some ideas on what I am doing wrong. The whole VCL sampleproject may be found here: https://github.com/stackoverflow-samples/OTLTaskException


Solution

  • This is an abstraction layer problem. Parallel.ParallelTask stores threaded code exception in a local field which is not synchronized with the IOmniTaskControl.FatalException property. (I do agree that this is not a good behaviour but I'm not yet sure what would be the best way to fix that.)

    Currently the only way to access caught exception of an IOmniParallelTask object is to call its WaitFor method. IOmniParallelTask should really expose a FatalException/DetachException pair, just like IOmniParallelJoin. (Again, an oversight, which should be fixed in the future.)

    The best way to solve the problem with the current OTL is to call WaitFor in the termination handler and catch the exception there.

    procedure TFormMain.HandleOnTaskThreadTerminated(const task: IOmniTaskControl);
    begin
      try
        fTask.WaitFor(0);
      except
        on E: Exception do
          memo.Lines.Add('an exception occured: ' + E.Message);
      end;
      CleanupTask;
    end;
    

    I have also removed the HandleOnTaskStop and moved the cleanup to the termination handler. Otherwise, fTask was already nil at the time HandleOnTaskThreadTerminated was called.


    EDIT

    DetachException, FatalException, and IsExceptional have been added to the IOmniParallelTask so now you can simply do what you wanted in the first place (except that you have to use the fTask, not task).

    procedure TFormMain.HandleOnTaskThreadTerminated(const task: IOmniTaskControl);
    begin
      if not assigned(fTask.FatalException) then
        Exit;
    
      memo.Lines.Add('an exception occured: ' + FTask.FatalException.Message);
      CleanupTask;
    end;
    

    EDIT2

    As noted in comments, OnTerminate handler relates to one task. In this example this is not a problem as the code makes sure that only one background task is running (NumTasks(1)).

    In a general case, however, the OnStop handler should be used for this purpose.

    procedure TFormMain.btnExecuteTaskClick(Sender: TObject);
    begin
      if Assigned(fTask) then
        Exit;
    
      memo.Lines.Add('task has been started..');
      fTask := Parallel.ParallelTask.NoWait.NumTasks(1);
      fTask.OnStop(HandleOnStop);
      fTask.Execute(TaskToExecute);
    end;
    
    procedure TFormMain.HandleOnStop;
    begin
      if not assigned(fTask.FatalException) then
        Exit;
    
      memo.Lines.Add('an exception occured: ' + FTask.DetachException.Message);
      TThread.Queue(nil, CleanupTask);
    end;
    

    As HandleOnStop is called in a background thread (because NoWait is used), CleanupTask must be scheduled back to the main thread, as in the original code.