How can I capture variables with an anonymous method when used in OTL?

What I want to do:

I have several objects in the list of genres. I want to capture each of these objects with an 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, the capture seems to work fine in the main thread. However, I do not know if the pointer to the object was captured or only its ID field?

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

First of all, only the third instance of TMyObject seemed to be captured. Secondly, I have four messages in the console log, despite the fact that I have only three objects in the general list. The second behavior is inconsistent, sometimes I have three messages in the console, sometimes I have four.

Please explain to me the cause of the two problems mentioned above and suggest a solution that fixes the problem and allows you to transfer each instance of the object to a separate OTL task. (I do not want to use the regular TThread class.)

+4
source share
2 answers

The documentation describes what happens :

Note that capturing variables captures variables, not values. If the value of a variable is changed after capture by creating an anonymous method, the value of the variable that the anonymous method also receives also changes because they are the same variable with the same storage.

There is only one LObject variable in your code, so all the anonymous methods that you create refer to it. As you reach your loop, the LObject value changes. Tasks have not yet been given the opportunity to start, so when they finally start, the loop ends, and LObject has its final meaning. Formally, this is the final value undefined after the loop.

To fix the value of a loop variable, complete 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 the loop code:

 CreateTask(CreateItemTask(LObject)).Unobserved.Run; 
+5
source

Anonymous procedures capture variables, not values. This way you grab the LObject variable. Since this is a loop variable, the value of LObject changes. Anonymous procedures evaluate LObject when they are executed, and not when creating anonymous procedures.

Instead of using an anonymous procedure, I would probably just use the TMyObject method. Try writing code this way and I predict that it will be easier for you 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 because WriteLn is not thread safe. Wrap the WriteLn call in a lock to clear it.

+1
source

Source: https://habr.com/ru/post/1445581/


All Articles