Tuesday, July 03, 2012

Better OnStop

Pop quiz! What’s wrong with this code?

  FPipeline1 := Parallel.Pipeline
.Stage(
procedure (const input: TOmniValue; var output: TOmniValue)
begin
end)
.Stage(
procedure (const input: TOmniValue; var output: TOmniValue)
begin
end)
.OnStop(
procedure
begin
Caption := 'All done';
FPipeline1 := nil;
end)
.Run;
FPipeline1.Input.CompleteAdding;

The answer (except the obvious one that the code does nothing useful) is that the OnStop delegate executes in the context of a worker thread. Because of that, assigning text to the form’s caption is not a safe operation. (Never access VCL from the worker thread!)

The official solution for this problem in release 3.01 was to assign a termination handler to the last stage. This is, however, not very intuitive and sometimes not very practical.

  FPipeline1 := Parallel.Pipeline
.Stage(
procedure (const input: TOmniValue; var output: TOmniValue)
begin
end)
.Stage(
procedure (const input: TOmniValue; var output: TOmniValue)
begin
end,
Parallel.TaskConfig.OnTerminated(
procedure
begin
Caption := 'All done';
FPipeline1 := nil;
end))
.Run;
FPipeline1.Input.CompleteAdding;

The same problem (OnStop executing from a worker thread) occurs in the parallel ForEach loop if it is executed with the NoWait modifier. In this case, however, the termination handler solution is even less practical as the same termination handler will be executed in all worker threads.

That’s why OmniThreadLibrary recently got support for a more powerful OnStop. ForEach was updated in revision 1184 and Pipeline in revision 1187. In both abstractions, OnStop will now accept a delegate with a single parameter of the IOmniTask type. OnStop will still execute in a background thread, but you’ll be able to use IOmniTask.Invoke to execute code fragment in the main thread.

  FPipeline1 := Parallel.Pipeline
.Stage(
procedure (const input: TOmniValue; var output: TOmniValue)
begin
end)
.Stage(
procedure (const input: TOmniValue; var output: TOmniValue)
begin
end)
.OnStop(
procedure (const task: IOmniTask)
begin
task.Invoke(
procedure
begin
Caption := 'All done';
FPipeline1 := nil;
end);
end)
.Run;
FPipeline1.Input.CompleteAdding;

Hmmm, maybe I should add OnStopInvoke which would wrap this ugliness?

5 comments:

  1. Anonymous14:46

    Hm. Maybe you should ;)
    Great stuff! Do you have any idea how to tame the Delphi code formatter which always wipes out any readability of anonymous methods?

    ReplyDelete
    Replies
    1. My "solution" is to never ever use Delphi code formatter.

      Delete
    2. Don't use it but use the one in GExperts and surround code, that should not be reformatted with {(*} and {*)}.

      Delete
  2. Maybe good solution is to add .AfterStop, where you can put code to be run on main thread?

    ReplyDelete
    Replies
    1. That's a good idea. I think I'll go with it. Thanks!

      Delete