1

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

P. Waksman
  • 979
  • 7
  • 21
  • All of these examples exhibit **undefined behavior** because you are accessing the `TMemo` from outside of the main UI thread. So all of the results will be random and could cause unexpected problems. You **must** synchronize with the main UI thread, such as with `TThread.Synchronize()`. But even then, you also need to take in account [how anonymous procedures bind to variables](http://docwiki.embarcadero.com/RADStudio/en/Anonymous_Methods_in_Delphi#Anonymous_Methods_Variable_Binding). – Remy Lebeau Mar 07 '17 at 01:22
  • So in my case the ObjectList with my objects would work just fine? And I'm having problems because I was trying to debug it with visual components? In this case Form1 with TMemo – P. Waksman Mar 07 '17 at 01:24
  • Probably not, or you wouldn't be asking this question in the first place. – Remy Lebeau Mar 07 '17 at 01:31
  • True, I'll read that anonymous bind that you post. – P. Waksman Mar 07 '17 at 01:33
  • Hmm, so changing the value of external variable cause the internal variable of anonymous method to change with it. That explain why I got the same values. A simple tweek resolves the problem. Instead of calling the Thread on the Loop, I change the "GO" method to call the thread, so I just make another method to be called with the Anonymous Thread. – P. Waksman Mar 07 '17 at 02:00

3 Answers3

1

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.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • hahaha, thanks didn't saw your answer in time, I make a very similar solution. I really need to take a look in deep delphi stuffs, this method pointers or anonymous bindings are not so simple. Thanks. – P. Waksman Mar 07 '17 at 02:15
  • "By moving the call to CreateAnonymousThread() into a separate procedure" that really resolve the problem. – P. Waksman Mar 07 '17 at 02:18
0

After reading a the article that Remy Lebeau post on the comments, I found this solution.

changing the main object by add one more procedure that make the call. Change the loop instead of creating the anonymous thread at the main loop, it is created inside the object.

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;

And the loop:

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;

Thats resolves the problem with just a minimum change on the Main Object.

P. Waksman
  • 979
  • 7
  • 21
0

This about race condition. When you increased to max value to 100, you will see different values. Threading not guarantee when Thread starts or ends. You can try this code block.

    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;

If you want a guarantee to write 1 to 4, you should instantiate every value before send to 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;