Wednesday, September 03, 2008

OmniThreadLibrary patterns - How to (not) create a task

From the first public release two months ago (or is it closer to three already?), OmniThreadLibrary has been constantly changing. Only with the 1.0 release it has reached a somewhat stable state. That flux was good as it was only through the development of the OTL that I discovered how to do some things properly (maybe I was not so wrong in Why is software third time lucky?, after all). But it was also bad as some introductionary topics that I wrote don't accurately reflect the current state anymore. Yes, I know, I should start writing good tutorials. Not fun :( I like writing blog articles more. Today I'll focus on task creation.

OTL offers three different ways to create a task (and a fourth one will be introduced when Delphi 2009 is available). Those three ways are mapped to three overloads of the CreateTask method (OtlTaskControl.pas).

  function CreateTask(worker: TOmniTaskProcedure; 
const taskName: string = ''): IOmniTaskControl; overload;
function CreateTask(worker: TOmniTaskMethod;
const taskName: string = ''): IOmniTaskControl; overload;
function CreateTask(const worker: IOmniWorker;
const taskName: string = ''): IOmniTaskControl; overload;

The simplest possible way (demoed in test application 2_TwoWayHello; see OmniThreadLibrary Example #3: Bidirectional communication) is to pass a name of a global procedure to the CreateTask. This global procedure must consume one parameter of type IOmniTask (it was described in the OmniThreadLibrary internals - OtlTask article, which is still mostly valid).

CreateTask(RunHelloWorld, 'HelloWorld').Run;

procedure RunHelloWorld(const task: IOmniTask);
begin
end;

A variation on the theme is passing a name of a method to the CreateTask. This approach is used in the test application 1_HelloWorld. The interesting point to make here is that you can declare this method in the same class from which the CreateTask is called. That way you can access the class fields and methods from the threaded code. Just keep in mind that you'll be doing this from another thread so make sure you know what you're doing!

procedure TfrmTestHelloWorld.RunHelloWorld(const task: IOmniTask);
begin
end;

For all except the simplest tasks, you'll use the third approach, because it will give you access to the true OTL power (internal wait loop and message dispatching). To use it, you have to create a worker object, which implements the IOmniWorker interface. An example from the 5_TwoWayHello_without_loop demo:

procedure TfrmTestTwoWayHello.actStartHelloExecute(Sender: TObject);
var
worker: IOmniWorker;
begin
worker := TAsyncHello.Create;
FHelloTask :=
OmniEventMonitor1.Monitor(CreateTask(worker, 'Hello')).
SetTimer(1000, MSG_SEND_MESSAGE).
SetParameter('Delay', 1000).
SetParameter('Message', 'Hello').
Run;
end;

As you're exposing this worker via interface, Delphi will manage its lifetime. When the task is terminated, worker instance will be destroyed too.

Because of the same reason, there's no need to create the worker class in advance. From the demo 7_InitTest:

task := OmniEventMonitor1.Monitor(CreateTask(TInitTest.Create(success), 'InitTest'))
.SetPriority(tpIdle).Run;

There's a catch, though. Because the CreateTask expects an interface, you must never pass to it a variable or field that is declared as a object. This will not work:

procedure TfrmTestTwoWayHello.actStartHelloExecute(Sender: TObject);
var
worker: TAsyncHello;
begin
worker := TAsyncHello.Create;
FHelloTask :=
OmniEventMonitor1.Monitor(CreateTask(worker, 'Hello')).
SetTimer(1000, MSG_SEND_MESSAGE).
SetParameter('Delay', 1000).
SetParameter('Message', 'Hello').
Run;
end;

If you need to store some information inside the worker during its execution and access it from the owner, you can use the Implementor property, which will "convert" IOmniTask interface back to the implementing object (which you must then cast to your worker class). From the demo 6_TwoWayHello_with_object_worker:

procedure TfrmTestTwoWayHello.actStopHelloExecute(Sender: TObject);
begin
FHelloTask.Terminate;
FHelloTask := nil;
lbLog.ItemIndex := lbLog.Items.Add(Format('%d Hello World''s sent',
[TAsyncHello(FWorker.Implementor).Count]));
end;

BTW, don't just set task reference (FHelloTask in the example above) to nil when you want to terminate the task. You have to call Terminate or use some other way to notify the task that it must terminate.

BTW2, if you use a parameterless constructor inside the CreateTask, you have to put empty parenthesis after the .Create or Delphi would not compile your code. I'm not really sure, why. The following segment is from test 14_TerminateWhen:

Log(Format('Task started: %d',
[CreateTask(TMyWorker.Create())
.TerminateWhen(FTerminate).WithCounter(FCounter)
.MonitorWith(OmniTED).Run.UniqueID]));

That completes today's tour.

In the introduction I mentioned the fourth CreateTask overload that will be introduced when Delphi 2009 is ready. OmniThreadLibrary will support anonymous methods so you'll be able to do something like that:

CreateTask(procedure begin MessageBeep(MB_ICONEXCLAMATION); end);

6 comments:

  1. Anonymous00:11

    Hello,

    Can we create a new task and provide this task to the thread pool "inside" a task (this one executed by the same thread pool)?

    Renaud

    ReplyDelete
  2. Interesting question. If the parent task will stay alive until subtask is completed, then it should work - if pool limits are set high enough so that subtask can start execution at all.

    What problem are you trying to solve?

    ReplyDelete
  3. Anonymous19:26

    My problem concern somes objects that communicate by a shared message list (thread safe), and i want that reading and writing message be executed in threads.

    So when my object write a new message in the list, it will be executed in a task, but just after writing the message in the list(end just before ended) i want that this objects create a new task( executed by in the thread pool), that correspond to the task of reading message in the shared list.

    Renaud

    ReplyDelete
  4. I still don't think I get it: you'll execute writer in a thread pool. Writer will write a message to a shared message list, start a reader (in the same thread pool) and terminate. Reader will then process the message.

    I think it should work. Try.

    ReplyDelete
  5. Anonymous03:05

    Thank for you response. I will try this.

    Renaud

    ReplyDelete
  6. If it doesn't work, send me the code so I can look at it. Best place for that is the OTL forum (http://otl.17slon.com/forum/).

    ReplyDelete