TThread.CreateAnonymousThread 的奇怪行为

Strange behavior with TThread.CreateAnonymousThread

我无法理解它是如何工作的。

首先举一个非常简单的例子,试图更好地解释我的情况。 此代码位于新项目中创建的新表单 Form1 中。其中 mmo1 是一个备忘录组件。

TOb = class
  Name : String;
  constructor Create(Name : String);
  procedure Go();
end;

procedure TOb.Go;
begin
  Form1.mmo1.Lines.Add(Name);
end;

然后我有这个事件的按钮:

procedure TForm1.btn4Click(Sender: TObject);
var
  Index : Integer;
begin
  mmo1.Lines.Clear;
  for Index := 1 to 3 do
    TThread.CreateAnonymousThread(TOb.Create('Thread ' + IntToStr(Index)).Go).Start;
end;

我在备忘录上的输出是:
线程 4
线程 4
线程 4

我真的不明白。

第一个问题:为什么"Name"输出是:线程4?是一个从1到3的For循环。至少应该是1或者3

第二个:为什么它只执行最后一个线程"Thread 4",而不是依次执行3次"Thread 1"、"Thread 2"、"Thread 3"?

我为什么要问这个?我有一个对象已经有一个运行良好的过程。但是现在我发现我需要处理这个对象的列表。当然一个接一个地工作很好,但在我的情况下它们是独立的,所以我认为 "hm, lets put them in threads, so it will run faster"。

为了避免修改对象以扩展 TThread 和覆盖 Execute 我查找如何使用过程而不是执行线程一个继承自 TThread 并找到匿名线程的对象。对一个对象非常有用,但是当我尝试循环遍历我的对象列表时,奇怪的行为发生了。

这个效果一样

  for Index := 1 to 3 do
    TThread.CreateAnonymousThread(
      procedure
      var
        Ob : TOb;
      begin
        OB := TOb.Create('Thread ' + IntToStr(Index));
        OB.Go;
      end
    ).Start;

当然我没有清理对象,这只是我进行的一些测试 运行。 有任何想法吗?或者在这种情况下,我需要继承 TThread 并覆盖 Execute 方法?

有趣的是 THIS 运行得很好。

mmo1.Lines.Clear;
TThread.CreateAnonymousThread(TOb.Create('Thread ' + IntToStr(1)).Go).Start;
TThread.CreateAnonymousThread(TOb.Create('Thread ' + IntToStr(2)).Go).Start;
TThread.CreateAnonymousThread(TOb.Create('Thread ' + IntToStr(3)).Go).Start;

输出:
线程 1
线程 2
线程 3

Works really great with one object, but when I tried loop through my object list, strange behaviors happens.

您可能没有考虑 how anonymous procedures bind to variables。特别是:

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.

例如,如果您这样做:

var
  Index: Integer;
begin
  for Index := 0 to ObjList.Count-1 do
    TThread.CreateAnonymousThread(TOb(ObjList[Index]).Go).Start;
end;

你实际上会在线程中导致 EListError 异常(我至少在测试它时 - 我不知道为什么会发生。通过之前向线程分配 OnTerminate 处理程序进行验证调用 Start(),然后让该处理程序检查 TThread(Sender).FatalException 属性).

如果改为这样做:

var
  Index: Integer;
  Ob: TOb;
begin
  for Index := 0 to ObjList.Count-1 do
  begin
    Ob := TOb(ObjList[Index]);
    TThread.CreateAnonymousThread(Ob.Go).Start;
  end;
end;

线程不会再崩溃,但它们很可能对同一个 TOb 对象进行操作,因为 CreateAnonymousThread() 正在引用 TOb.Go() 方法本身,并且然后你的循环在每次迭代中修改该引用的 Self 指针。我 怀疑 编译器可能会生成类似这样的代码:

var
  Index: Integer;
  Ob: TOb;
  Proc: TProc; // <-- silently added
begin
  for Index := 0 to ObjList.Count-1 do
  begin
    Ob := TOb(ObjList[Index]);
    Proc := Ob.Go; // <-- silently added
    TThread.CreateAnonymousThread(Proc).Start;
  end;
end;

如果您改为这样做,则会出现类似的问题:

procedure StartThread(Proc: TProc);
begin
  TThread.CreateAnonymousThread(Proc).Start;
end;

...

var
  Index: Integer;
  Ob: TOb;
begin
  for Index := 0 to ObjList.Count-1 do
  begin
    Ob := TOb(ObjList[Index]);
    StartThread(Ob.Go);
  end;
end;

可能因为编译器生成的代码类似这样:

procedure StartThread(Proc: TProc);
begin
  TThread.CreateAnonymousThread(Proc).Start;
end;

...

var
  Index: Integer;
  Ob: TOb;
  Proc: TProc; // <-- 
begin
  for Index := 0 to ObjList.Count-1 do
  begin
    Ob := TOb(ObjList[Index]);
    Proc := Ob.Go; // <-- 
    StartThread(Proc);
  end;
end;

不过这样会很好用:

procedure StartThread(Ob: TOb);
begin
  TThread.CreateAnonymousThread(Ob.Go).Start;
end;

...

var
  Index: Integer;
  Ob: TOb;
begin
  for Index := 0 to ObjList.Count-1 do
  begin
    Ob := TOb(ObjList[Index]);
    StartThread(Ob);
    // or just: StartThread(TOb(ObjList[Index]));
  end;
end;

通过将对 CreateAnonymousThread() 的调用移动到一个单独的过程中,将对 TOb.Go() 的实际引用隔离到局部变量中,您可以消除捕获多个对象的引用时发生冲突的任何可能性。

匿名程序很有趣。你必须小心他们如何捕获变量。

在阅读 Remy Lebeau post 的 article 评论后,我找到了这个解决方案。

通过添加一个进行调用的过程来更改主要对象。 更改循环而不是在主循环中创建匿名线程,它是在对象内部创建的。

TOb = class
  Name : String;
  constructor Create(Name : String);
  procedure Process();
  procedure DoWork();
end;

procedure TOb.Process;
begin
  TThread.CreateAnonymousThread(DoWork).Start;
end;

procedure TOb.DoWork;
var
  List : TStringList;
begin
  List := TStringList.Create;
  List.Add('I am ' + Name);
  List.Add(DateTimeToStr(Now));
  List.SaveToFile('D:\file_' + Name + '.txt');
  List.Free;
end;

循环:

List := TObjectList<TOb>.Create();
List.Add(TOb.Create('Thread_A'));
List.Add(TOb.Create('Thread_B'));
List.Add(TOb.Create('Thread_C'));
List.Add(TOb.Create('Thread_D'));

for Obj in List do
  //TThread.CreateAnonymousThread(Obj.Go).Start;
  Obj.Process;

只需对主要对象进行最小更改即可解决问题。

这是关于竞争条件的。当您将最大值增加到 100 时,您会看到不同的值。线程不保证线程何时开始或结束。 你可以试试这个代码块。

    for I := 1 to 100 do
  begin
    TThread.CreateAnonymousThread(
    procedure
    var
    Msg : string;
    begin
      try
        Msg := 'This' + I.ToString;
        MessageDlg(Msg,mtCustom,
                                [mbYes,mbAll,mbCancel], 0);
      Except
        on E: Exception do

      End;
    end
    ).Start;
  end;

如果你想保证写入 1 到 4,你应该在发送到 Thread 之前实例化每个值。

 for I := 1 to 100 do
  begin
    TThread.CreateAnonymousThread(
    procedure
    var
    Msg : string;
    begin
      var instanceValue := I;
      try
        Msg := 'This' + instanceValue.ToString;
        MessageDlg(Msg,mtCustom,
                                [mbYes,mbAll,mbCancel], 0);
      Except
        on E: Exception do

      End;
    end
    ).Start;
  end;