[fpc-pascal] Function reference doesn't capture in thread

Sven Barth pascaldragon at googlemail.com
Sat Sep 17 17:51:43 CEST 2022


Am 17.09.2022 um 17:04 schrieb Hairy Pixels:
>
>> On Sep 17, 2022, at 9:43 PM, Sven Barth <pascaldragon at googlemail.com> wrote:
>>
>> Then you didn't read my announcement mail deeply enough, cause it's mentioned there!
> Yes I just read that over and I remember it now. I feel like this not the same behavior in other languages although I’d need to test. Most recently I’m using Swift for my day job and I don’t recall ever having confusions like this come up.

Complain to the developers of Delphi then.

I do however plan to extend anonymous (and nested) functions with a 
syntax that allows to select whether a variable should be by-reference 
or by-value. But that's still a bit off, cause I have other things to do 
first.

>
>> And they are not pointers, but the variable is moved to an object instance which implements the interface of the function reference.
> “Moved to an object” makes me think the variable is copied but the reference behavior you describe sounds like a pointer so I’m still not 100% clear on what you mean.

Let's take your initial example and adjust it so that i will really be 
captured:

=== code begin ===

{$mode objfpc}
{$modeswitch anonymousfunctions}
{$modeswitch functionreferences}
{$modeswitch arrayoperators}

program test;
uses
   Cthreads, SysUtils, Classes;

type
   TProc = reference to procedure;
   TCallback = class(TThread)
     private
       proc: TProc;
     public
       constructor Create(p: TProc);
       procedure Execute; override;
end;

procedure TCallback.Execute;
begin
   proc();
   Sleep(100);
end;

constructor TCallback.Create(p: TProc);
begin
   inherited Create(true);

   proc := p;
end;

procedure DoTest;
var
   i: integer;
   callback: TCallback;
   callbacks: array of TCallback = ();
begin
   for i := 1 to 4 do
     begin
       callback := TCallback.Create(procedure
                   begin
                     writeln('Invoked: ', i, ' id: ', 
HexStr(TThread.CurrentThread));
                   end);

       callback.Start;
       callbacks += [callback];
     end;

   for i := 0 to High(callbacks) do
     callbacks[i].WaitFor;
end;

begin
   DoTest;
end.

=== code end ===

What the compiler will make of it will be as follows (the only 
difference will be inside DoTest):

=== code begin ===

procedure DoTest;

type
   TCapturer = class(TInterfacedObject, TProc)
     i: integer;
     procedure Anonymous1;

     procedure TProc.Invoke = Anonymous1;
   end;

   procedure TCapturer.Anonymous1;
   begin
     writeln('Invoked: ', i, ' id: ', HexStr(TThread.CurrentThread));
   end;

var
   capturer: TCapturer;
   capturer_keepalive: IUnknown;
   callback: TCallback;
   callbacks: array of TCallback = ();
begin
   capturer := TCapturer.Create;
   capturer_keepalive := capturer;
   for capturer.i := 1 to 4 do
     begin
       callback := TCallback.Create(capturer as TProc);

       callback.Start;
       callbacks += [callback];
     end;

   for capturer.i := 0 to High(callbacks) do
     callbacks[capturer.i].WaitFor;
end;

=== code end ===

That should make it clearer what is happening behind the scenes.

>>
>> Even so I’m trying now to modifying the example by copying to a local variable but I still see this same behavior suggesting there isn’t actually a copy. Why is this?
>>
>> When your callback is executed i might already have been changed!
> Ok that makes sense now since it’s just a reference. So this would have worked if I would have passed in i as a parameter and then stored it locally? I’m thinking now how I can preserve a copy of the state in the scope for each thread and I think local vars are the only way, correct? We need some more thread based examples to help people understand I think because I’m finding all sorts of edge cases updating a little thread library myself.

You need to move the creation of the thread to a separate nested 
function (cause each thread will then have a separate capture object; 
also this will work no matter if this is done in the main block or 
inside a procedure/function/method):

=== code begin ===

function CreateCallback(aIndex: Integer): TCallback;
begin
     Result := TCallback.Create(procedure
                 begin
                   writeln('Invoked: ', aIndex, ' id: ', 
HexStr(TThread.CurrentThread));
                 end);
end;

var
   i: integer;
   callback: TCallback;
   callbacks: array of TCallback = ();
begin
   for i := 1 to 4 do
     begin
       callback := CreateCallback(i);

       callback.Start;
       callbacks += [callback];
     end;

   for i := 0 to High(callbacks) do
     callbacks[i].WaitFor;
end.

=== code end ===

Regards,
Sven


More information about the fpc-pascal mailing list