How can I capture variables by anonymous method when using it in OTL?
Asked Answered
I

2

6

What I want to do:

I have a few objects in a genric list. I want to capture each of this object in anonymous method and execute this method as a separate OTL Task.

This is a simplified example:

program Project51;

{$APPTYPE CONSOLE}

uses
  SysUtils, Generics.Collections, OtlTaskControl, OtlTask;

type
  TProc = reference to procedure;

type
  TMyObject = class(TObject)
  public
    ID: Integer;
  constructor Create(AID: Integer);
  end;

constructor TMyObject.Create(AID: Integer);
begin
  ID := AID;
end;

var
  Objects: TList<TMyObject>;
  LObject: TMyObject;
  MyProc: TProc;
begin
  Objects := TList<TMyObject>.Create;
  Objects.Add(TMyObject.Create(1));
  Objects.Add(TMyObject.Create(2));
  Objects.Add(TMyObject.Create(3));
  for LObject in Objects do
  begin
    //This seems to work
    MyProc := procedure
    begin
      Writeln(Format('[SameThread] Object ID: %d',[LObject.ID]));
    end;
    MyProc;
    //This doesn't work, sometimes it returns 4 lines in console!?
    CreateTask(
      procedure(const Task: IOmniTask)
      begin
        Writeln(Format('[Thread %d] Object ID: %d',[Task.UniqueID, LObject.ID]));
      end
    ).Unobserved.Run;
  end;
  Sleep(500); //Just wait a bit for tasks to finish
  Readln;
end.

And this is the result:

Captured objects ID

As you can see, capturing seems to work fine in the main thread. However, I do not know if a pointer to an object has been captured or only its ID field?

When I try to capture the object and pass the anonymous method to CreateTask function things become weird.

First of all, only the third instance of TMyObject seemed to be captured. Second of all, I've got four messages in console log despite the fact that I have only three objects in generic list. The second behaviour is inconsistent, sometimes I've got three messages in console, sometimes I've got four.

Please explain me the reason for two issues mentioned above and propose a solution that eliminates the problem and allows me to pass each instance of object to a separate OTL task. (I do not want to use regular TThread class.)

Ilailaire answered 12/11, 2012 at 18:1 Comment(0)
S
6

The documentation describes what's happening:

Note that variable capture captures variables—not values. If a variable's value changes after being captured by constructing an anonymous method, the value of the variable the anonymous method captured changes too, because they are the same variable with the same storage.

In your code, there is only one LObject variable, so all the anonymous methods you construct refer to it. As your loop makes progress, the value of LObject changes. The tasks haven't gotten a chance to start running yet, so when they do finally run, the loop has terminated and LObject has its final value. Formally, that final value is undefined after the loop.

To capture the value of the loop variable, wrap creation of the task in a separate function:

function CreateItemTask(Obj: TMyObject): TOmniTaskDelegate;
begin
  Result := procedure(const Task: IOmniTask)
            begin
              Writeln(Format('[Thread %d] Object ID: %d',[Task.UniqueID, Obj.ID]));
            end;
end;

Then change your loop code:

CreateTask(CreateItemTask(LObject)).Unobserved.Run;
Surprisal answered 12/11, 2012 at 18:41 Comment(2)
Thank you Rob, what I really want to capture (maybe I was not clear enough) is the address of the memory under which I can find the instance of TMyObject, because I want to use methods and properties of this object, not only its ID. But it should not be hard to modify your example, right?Ilailaire
Not hard at all; see the edited answer. To capture by value instead of by reference, pass the desired value to the method-reference-creating function. In your case, the value you wish to capture is the object reference stored in LObject.Surprisal
S
1

Anonymous procedures captures variables rather than values. So you are capturing the variable LObject. Since this is a loop variable, the value of LObject changes. The anonymous procedures evaluate LObject when they execute rather than when the anonymous procedures are created.

Rather than using an anonymous procedure, I'd probably just use a method of TMyObject. Try writing the code that way and I predict you will find it easier to understand.

procedure TMyObject.TaskProc(const Task: IOmniTask);
begin
  Writeln(Format('[Thread %d] Object ID: %d', [Task.UniqueID, Self.ID]));
end;

The reason for getting 4 lines of output rather than 3 is probably just that WriteLn is not threadsafe. Wrap the call to WriteLn in a lock to clear that up.

Spheroidicity answered 12/11, 2012 at 18:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.