【问题标题】:How the variable capturing in anonymous functions works in Delphi TTask - PPL?匿名函数中的变量捕获如何在 Delphi TTask - PPL 中工作?
【发布时间】:2022-01-30 11:39:56
【问题描述】:

我开始深入研究 Delphi (D11) PPL 并写了这个小例子:

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;

这是运行程序后调试窗口显示的内容:

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

怎么可能?我很可能错过了一些明显的东西......

【问题讨论】:

    标签: delphi capture ppl


    【解决方案1】:

    Embarcadero 的文档对此进行了详细介绍:

    Anonymous Methods in Delphi: Variable Binding Mechanism

    如果匿名方法在其主体中引用外部局部变量,则该变量被“捕获”。捕获意味着延长变量的生命周期,使其与匿名方法值一样长,而不是随着它的声明例程而死。 请注意,变量捕获捕获的是变量——而不是。如果构造匿名方法捕获后变量的值发生变化,那么匿名方法捕获的变量的值也会发生变化,因为它们是同一个变量,具有相同的存储空间。捕获的变量存储在堆上,而不是存储在堆上。堆栈。

    然后文档更详细地解释了实现如何捕获变量,尤其是当多个匿名方法捕获相同的变量时(如您的示例中的情况)。

    这种情况在多个匿名方法捕获同一个局部变量的情况下更加复杂。要了解它在所有情况下的工作原理,有必要更准确地了解实现的机制。

    (详情如下...)

    因此,您的问题与 TTask/PPL 本身无关,而与您正在创建多个共享 j 变量的匿名过程有关。他们正在捕获对变量本身的引用,而不是它的值。当所有任务完成休眠时,j 已设置为其最终值,然后所有任务都会输出该值。

    解决方法(在上面的同一文档中也有描述)是让您的循环将变量作为参数传递给中间函数,然后声明匿名方法来捕获参数。这样,每个匿名方法都会捕获不同的变量,例如:

    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;
    

    另一种解决方案是将循环替换为TParallel.For(),而不是直接使用TTask,例如:

    procedure TForm2.LaunchTasks;
    const
      cmax = 5;
    begin
      TParallel.&For(1, cmax,
        procedure (index: integer)
        begin
          Sleep(3000);
          SendDebugFmt('Task #%d' , [index]);
        end);
    end;
    

    【讨论】:

    • 嗨,杰里米,感谢您的回答。我现在明白它是如何工作的。那么,如果有人问,应该怎么做才能将变量“值”传递给处理程序呢?看起来线程队列的使用是不可避免的。还是有一些不太复杂的解决方案?
    • @user8934301 "嗨,Jeremy" - 名字错误,再看一遍。 “应该怎么做才能将变量值传递给处理例程?” - 解决方案与我在回答中链接到的文档相同。让您的循环将变量作为参数传递给一个中间函数,该函数声明匿名方法来捕获参数。这样,每个匿名方法都在捕获不同的变量。另一种解决方案是将循环替换为TParallel.For(),而不是直接使用TTask
    • 哎呀,对不起,名字拼错了,雷米。我查看了这个论坛中的示例和其他主题,发现这里的解决方案是使用带有返回正确 TProc 的值参数的中间函数。正如你所建议的。我不能使用 TParallel.For 因为实际上我有更多变量而且它们不是整数。非常感谢您的耐心等待。
    • @user8934301 "我不能使用 TParallel.For,因为实际上我有更多变量,它们不是整数" - TParallel.For() 有一个可选的 Sender 参数您可以使用将用户定义的数据发送到匿名方法中,它不必只接受循环索引作为参数。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-07-06
    • 2019-03-09
    • 2012-11-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-10
    相关资源
    最近更新 更多