Thursday, February 19, 2015

PPL - TTask Exception Management

Recently I wrote a blog post titled: "PPL - TTask an example in how not to use." The goal of that post was to help introduce some of the new thought processes that are required around multi-threaded programming.

To keep GUI code responsive, threads can be used to keep time consuming code out of the Main thread where the GUI code runs. For example a good usage for this is database access, and calling web services. But, what happens when the database access or web service call fails?   Using the same methodology as the prior blog post of doing it wrong first, this blog post now exists.

I have modified the code from the prior blog post, where we dropped a listbox and button on a form. The new code now raises an exception during the execution.
 
procedure TForm5.Button1Click(Sender: TObject);
begin
  Button1.Enabled := False;
  SlowProc;
end;

procedure TForm5.FormDestroy(Sender: TObject);
begin
  Task.Cancel;
end;

procedure TForm5.SlowProc;
begin
 Task := TTask.Create( procedure
                var
                   I : Integer;
                begin
                  for I := 0 to 9 do
                  begin
                     if TTask.CurrentTask.Status = TTaskStatus.Canceled then
                        exit;
                     Sleep(1000);
                     if I = 2 then
                        raise EProgrammerNotFound.Create('Something bad just happened');
                  end;
                  if TTask.CurrentTask.Status <> TTaskStatus.Canceled then
                  begin
                    TThread.Queue(TThread.CurrentThread,
                    procedure
                    begin
                      if Assigned(ListBox1) then
                      begin
                        Listbox1.Items.Add('10 Seconds');
                        Button1.Enabled := True;
                      end;
                    end);
                 end;
              end);
 Task.Start;
end;
When we run this code and press the button on the form the button is disabled and then nothing happens. The user gets no notification of the error. That is because the TTask has no way to notify the GUI of the exception. That is up to the developer. Never fail I know how exceptions work just wrap the code with a TRY EXCEPT block and raise it in the main thread.
 
procedure TForm5.SlowProc;
begin
 Task := TTask.Create( procedure
                var
                   I : Integer;
                begin
                  try
                    for I := 0 to 9 do
                    begin
                       if TTask.CurrentTask.Status = TTaskStatus.Canceled then
                          exit;
                       Sleep(1000);
                       if I = 2 then
                          raise EProgrammerNotFound.Create('Something bad just happened');
                    end;
                    if TTask.CurrentTask.Status <> TTaskStatus.Canceled then
                    begin
                      TThread.Queue(TThread.CurrentThread,
                      procedure
                      begin
                        if Assigned(ListBox1) then
                        begin
                          Listbox1.Items.Add('10 Seconds');
                          Button1.Enabled := True;
                        end;
                      end);
                   end;
                 except
                  on E : Exception do
                  begin
                      TThread.Queue(TThread.CurrentThread,
                      procedure
                      begin
                        raise E;
                      end);
                  end;
                 end;
              end);
 Task.Start;
end;

The application is run the application and get some ugly error like this one. "Exception TForm5.SlowProc$2$ActRec.$0$Body$3$ActRec in module Project4.exe at 00208756."

The reason we don't get the correct errors is that the variable of E that is created during the during the TRY EXCEPT block is freed by the time the main thread gets around to raising the exception.

So we try changing this segment of the code from this:
 
TThread.Queue(TThread.CurrentThread,
    procedure
    begin
       raise E;
    end);
to
     
TThread.Synchronize(TThread.CurrentThread,
                      procedure
                      begin
                        raise E;
                      end);
Because the Synchronize will halt the current thread and wait for the main thread to execute the the synchronized code. But we run the code and we are back to nothing happening again, but why?

This is because Synchronize captures the exception and re-raises the exception in the originating thread.

AcquireExceptionObject function to the rescue.

Calling AcquireExceptionObject allows you increment the Exception Object reference count so that it's not destroyed at the end of the TRY EXCEPT Block.   Then we can call TThread.Queue and raise the exception in the main thread.
 
procedure TForm5.SlowProc;
begin
 Task := TTask.Create( procedure
                var
                   I : Integer;
                   CapturedException : Exception;
                begin
                  try
                    for I := 0 to 9 do
                    begin
                       if TTask.CurrentTask.Status = TTaskStatus.Canceled then
                          exit;
                       Sleep(1000);
                       if I = 2 then
                          raise EProgrammerNotFound.Create('Something bad just happened');
                    end;
                    if TTask.CurrentTask.Status <> TTaskStatus.Canceled then
                    begin
                      TThread.Queue(TThread.CurrentThread,
                      procedure
                      begin
                        if Assigned(ListBox1) then
                        begin
                          Listbox1.Items.Add('10 Seconds');
                          Button1.Enabled := True;
                        end;
                      end);
                   end;
                 except
                     CapturedException := AcquireExceptionObject;
                     TThread.Queue(TThread.CurrentThread,
                     procedure
                     begin
                       if Assigned(Button1) then 
                          Button1.Enabled := true;
                       raise CapturedException;
                     end);
                  end;
              end);
 Task.Start;
end;
Now when something bad happens in our task the GUI is notified.  Problem solved! But it's not the whole story, there are other ways to manage exceptions with TTasks, and depending on the nature of your code you this option may be better.

You can remove the TRY EXCEPT Block. When a TTask is executed your user code is already wrapped in a TRY EXCEPT block, and it captures the exception for you already.

If I have a reference to the Task I can call Task.Wait(TimeoutValue), which will wait for the time out for the task to complete and return true if it completed.  If it has stopped executing due to an exception an EAggregateException will be raised in the thread that called Task.Wait() if that is the main thread then the user would be notified of the problem.

TTask has the ability to have N number of child tasks. Because of this exceptions that are raised in a TTask are aggregated together in an EAggregateException object. The EAggregateException is defined with the following public interface.
 
  EAggregateException = class(Exception)
  public type
    TExceptionEnumerator = class
    public
      function MoveNext: Boolean; inline;
      property Current: Exception read GetCurrent;
    end;
  public
    constructor Create(const AExceptionArray: array of Exception); overload;
    constructor Create(const AMessage: string; const AExceptionArray: array of Exception); overload;
    destructor Destroy; override;

    function GetEnumerator: TExceptionEnumerator; inline;
    procedure Handle(AExceptionHandlerEvent: TExceptionHandlerEvent); overload;
    procedure Handle(const AExceptionHandlerProc: TExceptionHandlerProc); overload;
    function ToString: string; override;
    property Count: Integer read GetCount;
    property InnerExceptions[Index: Integer]: Exception read GetInnerException; default;
  end;

With this interface a developer can loop through each individual exceptions, or call .ToString which places all the exception messages into a single string.

Hopefully this give a few more bits of insight into exception management with threads and TTask.


2 comments:

  1. The exception handling in PPL is still buggy: http://qc.embarcadero.com/wc/qcmain.aspx?d=129159
    In your example this is not a problem, because the exception was caught within task's handler. But if the exception escapes the handler then the result will be a memory leak.

    ReplyDelete