Search code examples
delphi-xe2tthread

Strange behavior with TThread.CreateAnonymousThread


I was unable to follow how it is working.

A very simple example first, to try explain my situation better. This code is inside a new Form Form1 create in a new project. Where mmo1 is a Memo component.

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

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

Then I have a button with this event:

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;

And my output on the memo is:
Thread 4
Thread 4
Thread 4

I really don't got it.

First question: Why the "Name" output is: Thread 4? Is a For loop from 1 to 3. At least should be 1 or 3

Second: Why it only execute the last thread "Thread 4", instead of 3 times in sequence "Thread 1", "Thread 2", "Thread 3"?

Why I'm asking this? I have an object that has already a process working fine. But now I found me in a situation that I need a List of this object to be processed. Sure work fine process one by one, but in my case they are independent one of other so I thought "hm, lets put them in threads, so it will run faster".

To avoid modifying the object to extend TThread and overriding Execute I look up on how to execute a thread with a procedure instead of an object that inherits from TThread and found the Anonymous Thread. Works really great with one object, but when I tried loop through my object list, strange behaviors happens.

This has the same effect.

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

Sure I'm not clean the object, this was just some tests that I was running. Any Ideas? Or in this case I will need to inherits from TThread and override the Execute methode?

The funny thing is that THIS runs just fine.

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;

Output:
  Thread 1
  Thread 2
  Thread 3


Solution

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

    You are likely not taking into account how anonymous procedures bind to variables. In particular:

    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.

    For example, if you do something like this:

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

    You will actually cause an EListError exception in the threads (I least when I tested it - I don't know why it happens. Verified by assigning an OnTerminate handler to the threads before calling Start(), and then having that handler check the TThread(Sender).FatalException property).

    If you do this instead:

    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;
    

    The threads won't crash anymore, but they are likely to operate on the same TOb object, because CreateAnonymousThread() is taking a reference to the TOb.Go() method itself, and then your loop is modifying that reference's Self pointer on each iteration. I suspect the compiler is likely generating code similar to this:

    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;
    

    If you do this instead, it will have a similar issue:

    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;
    

    Probably because the compiler generates code similar to this:

    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;
    

    This will work fine, though:

    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;
    

    By moving the call to CreateAnonymousThread() into a separate procedure that isolates the actual reference to TOb.Go() into a local variable, you remove any chance of conflict in capturing the reference for multiple objects.

    Anonymous procedures are funny that way. You have to be careful with how they capture variables.