Monday, November 30, 2009

OmniThreadLibrary patterns – Task controller needs an owner

Pop quiz. What’s wrong with this code?

CreateTask(MyWorker).Run;

Looks fine, but it doesn’t work. In most cases, running this code fragment would cause immediate access violation.

This is a common problem amongst new OTL users. Heck, even I have fallen into this trap!

The problem here is that CreateTask returns IOmniTaskControl interface, or task controller. This interface must be stored into some persistent location, or task controller would be destroyed immediately after Run is called (because the reference count would fall to 0).

A common solution is to just store the interface in some field.

FTaskControl := CreateTask(MyWorker).Run;

When you don’t need background worker anymore, you should terminate the task and free the task controller.

FTaskControl.Terminate;

FTaskControl := nil;

This works for background workers with long life span – for example if there’s a background thread running all the time the program itself is running. But what if you are starting a short-term background task? In this case you should monitor it with TOmniEventMonitor and cleanup task controller reference in OnTerminate event handler.

FTaskControl := CreateTask(MyWorker).MonitorWith(eventMonitor).Run;

In eventMonitor.OnTerminate:

FTaskControl := nil;

As it turns out, event monitor keeps task controller interface stored in its own list, which will also keep the task controller alive. That’s why the following code also works.

CreateTask(MyWorker).MonitorWith(eventMonitor).Run;

Since OTL v1.04 you have another possibility – write a method to free the task controller and pass it to the OnTerminated.

FTaskControl := CreateTask(MyWorker).OnTerminated(FreeTaskControl).Run;

procedure FreeTaskControl(const task: IOmniTaskControl);
begin
  FTaskControl := nil;
end;

If you’re using Delphi 2009 or 2010, you can put the cleanup code in anonymous method.

FTaskControl := CreateTask(MyWorker).OnTerminated(
procedure(const task: IOmniTaskControl) begin
  FTaskControl := nil;
end)
.Run;

OnTerminated does its magic by hooking task controller into internal event monitor. Therefore, you can get real tricky and just write “null” OnTerminated.

CreateTask(MyWorker).OnTerminated(DoNothing).Run;

procedure DoNothing(const task: IOmniTaskControl);
begin
end;

As that looks quite ugly, I’ve added method Unobserved just few days before version 1.04 was released. This method does essentially the same as the “null” OnTerminated approach, except that the code looks nicer and programmers intentions are more clearly expressed.

CreateTask(MyWorker).Unobserved.Run;

7 comments:

  1. Ritsaert Hornstra09:09

    Should Run not add a reference to the interface instance and remove that reference when done?

    ReplyDelete
  2. Then you end with task controller referencing itself which causes all sorts of problems when reference count drops to zero.

    I was thinking more along the lines of .Unobserved being implicitly called in all occasions but I don't know yet if that's a good idea in all cases.

    ReplyDelete
  3. Anonymous11:16

    But you can call TInterfacedObject._AddRef in Run procedure and TInterfacedObject._Release in Terminate procedure?

    ReplyDelete
  4. Been there, cause incredible lots of problems and pain (for me), won't try it again...

    ReplyDelete
  5. When using TInterfacedObject I got more problems than advantages. These errors are very difficult to find. So I wrote a different IUnknown implementation with no references count.

    BTW in Delphi 2010 I found Embarcadero's similar implementation :)

    ReplyDelete
  6. Reference counting is OK. OTL is designed around that concept. It's just sometimes that you have to keep this in mind ...

    ReplyDelete
  7. Anonymous17:05

    Doubt: You say "CreateTask(MyWorker).Run;" will cause access violation. But the first example Beep uses a code exactly like that?

    ReplyDelete