0

I started to dig into Delphi (D11) PPL and wrote this small example:

procedure TForm2.LaunchTasks;
const
  cmax = 5;
Var
  ltask: ITask;
  i,j: Integer;
begin
  for i := 1 to cmax do
  begin
    j := i;
    ltask := TTask.Create(
      procedure ()
      begin
        Sleep(3000);
        SendDebugFmt('Task #%d' ,[j])
      end);
    ltask.Start;
  end;
end;

This is what debug window shows after running the procedure:

Task #5
Task #5
Task #5
Task #5
Task #5

how could it be? I most probably miss something obvious...

1 Answers1

5

Embarcadero's documentation covers this in detail:

Anonymous Methods in Delphi: Variable Binding Mechanism

If an anonymous method refers to an outer local variable in its body, that variable is "captured". Capturing means extending the lifetime of the variable, so that it lives as long as the anonymous method value, rather than dying with its declaring routine. 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. Captured variables are stored on the heap, not the stack.

The documentation then goes into greater detail explaining exactly how the implementation captures variables, especially when multiple anonymous methods capture the same variable (as is the case in your example).

This situation is more complicated in the case of multiple anonymous methods capturing the same local variable. To understand how this works in all situations, it is necessary to be more precise about the mechanics of the implementation.

(details follow...)

So, your issue has nothing to do with TTask/PPL itself, and everything to do with the fact that you are creating multiple anonymous procedures that share the j variable. They are capturing a reference to the variable itself, not its value. By the time all of the tasks have finished sleeping, j has been set to its final value, which all of the tasks then output.

The fix (which is also described in the same documentation above) is to have your loop pass the variable as a parameter to an intermediate function which then declares the anonymous method to capture the parameter. That way, each anonymous method is capturing a different variable, eg:

procedure LaunchTask(index: Integer);
var
  ltask: ITask;
begin
  ltask := TTask.Create(
    procedure ()
    begin
      Sleep(3000);
      SendDebugFmt('Task #%d' , [index]);
    end);
  ltask.Start;
end;

procedure TForm2.LaunchTasks;
const
  cmax = 5;
Var
  i: Integer;
begin
  for i := 1 to cmax do
  begin
    LaunchTask(i);
  end;
end;

An alternative solution is to replace your loop with TParallel.For() instead of using TTask directly, eg:

procedure TForm2.LaunchTasks;
const
  cmax = 5;
begin
  TParallel.&For(1, cmax,
    procedure (index: integer)
    begin
      Sleep(3000);
      SendDebugFmt('Task #%d' , [index]);
    end);
end;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Hi Jeremy, thanks for the answer. I understand now how it works. So, if may ask, what should one do to pass the variable "value" to the processing routine? Looks like the use of thread queue is inevitable. Or is there some less complicated solution? – user8934301 Jan 31 '22 at 06:50
  • @user8934301 "*Hi, Jeremy*" - wrong name, look again. "*what should one do to pass the variable value to the processing routine?*" - the solution is in the same documentation I linked to in my answer. Have your loop pass the variable as a parameter to an intermediate function which declares the anonymous method to capture the parameter. That way, each anonymous method is capturing a different variable. An alternative solution is to replace your loop with [`TParallel.For()`](https://docwiki.embarcadero.com/Libraries/en/System.Threading.TParallel.For) instead of using `TTask` directly. – Remy Lebeau Jan 31 '22 at 10:41
  • oops, sorry for misspelling the name, Remy. I looked at the examples and other topics in this forum and have found that the solution here is using intermediate function with by value params that returns correct TProc. As you suggested. I canot use TParallel.For because in real I have more variables and they are not not integers. Many thanks for your patience. – user8934301 Jan 31 '22 at 10:54
  • @user8934301 "*I canot use TParallel.For because in real I have more variables and they are not not integers*" - `TParallel.For()` has an optional `Sender` parameter that you can use to send user-defined data into the anonymous method, it doesn't have to accept only the loop index as a parameter. – Remy Lebeau Jan 31 '22 at 11:03