Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Mailing lists / lazarus / 2017-06

Enqueuing a callback from a thread via other class - or am I overcomplicating this ?

10 messages · 6 people · –

Reply by email Subscribe to this list Your subscription

Read-only archive. Use j and k to move between messages.

Lukasz Sokol

Hi,

I have a TThread, that processes a T(Thread)List of TSomeClass objects continuously.

I want to have a callback in the TSomeClass, to which another class can put their procedure to call into, but the callback needs to be 'thread safe' (because it can be accessed (read) by GUI related elements).

I tried like this:

TSomeClass = class

private
... // other variables/fields
public
  class var ProcessedCallback : TThreadMethod;
  var
   ... //other variavbles
  procedure Processed;
end;
procedure TSomeClass.Processed;
begin
  if Assigned(ProcessedCallback) then
    ProcessedCallback();
end;

and in the TMyThread I am trying to do

var SomeClassList : TThreadList; // is declared private to the thread - only the thread manages the lifetime of the list members
procedure TMyThread.Execute;
var
    SomeClassLockedList: TList;
begin
  i := 0;
  while not Terminated do
    begin
      if SomeClassList.Count = 0 then
      begin
        sleep(10);
        continue;
      end;
      SomeClassLockedList := SomeClassList.LockedList; // there are try/excepts around all here, but did not want to muddle the picture
      Queue(TSomeClass(SomeClassLockedList.Items[i]).Processed);
      // but above here I'm getting an error : got 'untyped', expected 'procedure variable type of object;Register'.
      SomeClassList.UnlockList;
      i := i+1;
    end;
end;
// the above sort-of gives the gist of what I'm trying to achieve

What am I doing wrong ?
Does the procedure to be enqueued have to be declared in the TThread header ?

Hope some of it makes (any) sense ...

Thanks in advance,

-L.

Reply by email

Mattias Gaertner
to Lukasz Sokol
[...] SomeClassLockedList := SomeClassList.LockedList; // there are try/excepts around all here, but did not want to muddle the picture Queue(TSomeClass(SomeClassLockedList.Items[i]).Processed);
Queue(@TSomeClass(SomeClassLockedList.Items[i]).Processed);
      ^

Mattias

Reply by email

Lukasz Sokol
to Mattias Gaertner
Mattias Gaertner wrote 12 lines
On Wed, 14 Jun 2017 11:12:11 +0100 Lukasz Sokol via Lazarus wrote: > [...] > SomeClassLockedList := SomeClassList.LockedList; // there are try/excepts around all here, but did not want to muddle the picture > Queue(TSomeClass(SomeClassLockedList.Items[i]).Processed); Queue(@TSomeClass(SomeClassLockedList.Items[i]).Processed); ^ Mattias

Thanks! looks so easy in hindsight ;) same the callbacks need to be assigned with the @ in front :)

-L.

Reply by email

el es
to Lukasz Sokol

Hindsight, schmindsight, but here is another question:

Where does the call queued from a thread, return to ?

As in, the thread calls Queue() on a method of the object we're interested in; and that in turn calls a method / procedure more like / from the 'main form' unit.

The object's lifetime is controlled by the dedicated thread
(meaning only one thread is allowed to Free() it);

But when, during the course of Queue()d callback,
I FreeAndNil() the object that had the pointer to the callback procedure, I get SIGSEGV pointing at CheckSynchronize in Application. (address F0F0F0F0).

I guess, easiest way to avoid this, is 'don't do that', so i did, and the SIGSEGV went away.

Hence the question is, how can I know it is safe to free the object, that has the callback subscribed to it?
One of the ways would be, to have a flag in that object, that the callback proc will set, and so the thread will know it's now safe to Free(andNil)() it.

But are there any other, better?

Thanks in advance,
-L.

Lukasz Sokol wrote 18 lines
On 14/06/17 11:33, Mattias Gaertner via Lazarus wrote: > On Wed, 14 Jun 2017 11:12:11 +0100 > Lukasz Sokol via Lazarus wrote: > >> [...] >> SomeClassLockedList := SomeClassList.LockedList; // there are try/excepts around all here, but did not want to muddle the picture >> Queue(TSomeClass(SomeClassLockedList.Items[i]).Processed); > > Queue(@TSomeClass(SomeClassLockedList.Items[i]).Processed); > ^ > > Mattias > Thanks! looks so easy in hindsight ;) same the callbacks need to be assigned with the @ in front :) -L.

Reply by email

Juha Manninen
to el es
el es wrote
But when, during the course of Queue()d callback, I FreeAndNil() the object that had the pointer to the callback procedure, I get SIGSEGV pointing at CheckSynchronize in Application. (address F0F0F0F0).

I don't know the details of this issue but address F0F0F0F0 means you used compiler flag -gt (trash variables) and the variable is uninitialized or already freed.
Yes, the flag -gt is good, helps find such errors.

Show quoted text 5 lines
Hence the question is, how can I know it is safe to free the object, that has the callback subscribed to it? One of the ways would be, to have a flag in that object, that the callback proc will set, and so the thread will know it's now safe to Free(andNil)() it.

A flag in an uninitialized or freed object does not help.

Juha

Reply by email

el es
to Juha Manninen
Juha Manninen wrote 11 lines
On Sat, Jun 17, 2017 at 11:25 PM, el es via Lazarus wrote: > But when, during the course of Queue()d callback, > I FreeAndNil() the object that had the pointer to the callback procedure, I > get SIGSEGV pointing at CheckSynchronize in Application. (address F0F0F0F0). I don't know the details of this issue but address F0F0F0F0 means you used compiler flag -gt (trash variables) and the variable is uninitialized or already freed. Yes, the flag -gt is good, helps find such errors.

It came I think, with default build settings in Lazarus 1.6.(2 i think) I installed recently

Show quoted text 9 lines
> Hence the question is, how can I know it is safe to free the object, > that has the callback subscribed to it? > One of the ways would be, to have a flag in that object, that the > callback proc will set, and so the thread will know it's now safe to > Free(andNil)() it. A flag in an uninitialized or freed object does not help.

No, I mean, the callback (executed in main thread context) must not indicate that the object is ready to be freed (because the thread managing the objects' lifetime may free it before the callback returns).

Hence the object, would have to have ITS callback routine be something like a TNotifyEvent ( procedure(AObject: TSomeObject) of object, or something), and the flag set is set when the callback returns to the object context like

procedure TSomeObject.Callback;
// this procedure is enQueue()d by the thread
begin
   if Assigned({Self.}FSomeObjectNotifyEvent) then
     FSomeObjectNotifyEvent(Self);
    // this is what gets executed in the MainForm
   // but then we come back here right ?
   // but then again setting the CanDestruct flag here
   // may be still too early...
   /// so I don't know...
   // unless - may there be any procedure/call I can place in
   // the FSomeObjectNotifyEvent handler, that will cause the
   // execution to NOT come back here? the reverse of 'fork'...
end;
Juha

Thanks in advance
-L.

Reply by email

José Mejuto
to el es
el es wrote 10 lines
Hence the object, would have to have ITS callback routine be something like a TNotifyEvent ( procedure(AObject: TSomeObject) of object, or something), and the flag set is set when the callback returns to the object context like procedure TSomeObject.Callback; // this procedure is enQueue()d by the thread begin if Assigned({Self.}FSomeObjectNotifyEvent) then FSomeObjectNotifyEvent(Self);

Hello,

Just exit the procedure and there is not free problem. You will get an exception when you use object data, not object code.

procedure TSomeObject.Callback;
// this procedure is enQueue()d by the thread
begin
    if Assigned({Self.}FSomeObjectNotifyEvent) then
      FSomeObjectNotifyEvent(Self);
    Self.CanBeDestroyed := true;
    Exit;
end;

After you set CanBeDestroyed to true you must consider all data as RANDOM, so don't access any property or variable.

Remember that creating and destroying threads is very expensive in CPU time.

Reply by email

Lukasz Sokol
to José Mejuto
José Mejuto wrote 14 lines
El 18/06/2017 a las 22:44, el es via Lazarus escribió: > Hence the object, would have to have ITS callback routine be something like a TNotifyEvent ( procedure(AObject: TSomeObject) of object, or something), and the flag set is set when the callback returns to the object context like > > procedure TSomeObject.Callback; > // this procedure is enQueue()d by the thread > begin > if Assigned({Self.}FSomeObjectNotifyEvent) then > FSomeObjectNotifyEvent(Self); Hello, Just exit the procedure and there is not free problem. You will get an exception when you use object data, not object code.
I did now, and also, beforehands I did not have the TSomeObjectNotifyEvent() type of callback...
so in the callback I had to browse through the list of TSomeObject's, searching for the right one - and that was really risky;
Show quoted text 12 lines
procedure TSomeObject.Callback; // this procedure is enQueue()d by the thread begin if Assigned({Self.}FSomeObjectNotifyEvent) then FSomeObjectNotifyEvent(Self); Self.CanBeDestroyed := true; Exit; end; After you set CanBeDestroyed to true you must consider all data as RANDOM, so don't access any property or variable.

Understood, I ensured there is nothing accessing the TSomeObject sent as Self to the event handler, after it comes back.

Remember that creating and destroying threads is very expensive in CPU time.

There is only 2 statically defined threads in the entire application :)

one is the GUI thread (the Application, aka main thread)

and the other is the TSomeObject life handler
(you could call it a queue of objects to be processed and returning results from hardware communication)

Thanks,

-L.

Reply by email

Michael Schnell
to el es: Hindsight, schmindsight, but here is another question: Where does the call queued from a…
el es wrote
Where does the call queued from a thread, return to ?

From the POV of the application programmer: "nowhere". it's just another (main-Thread-) "Event" that (like "OnClick") gets "fired" by the Lazarus/fpc infrastructure and is done.

The object's lifetime is controlled by the dedicated thread (meaning only one thread is allowed to Free() it);

Of course a queued event should not use an object that might get freed by other means.

If you want to pass data to the event, a useful way to avoid this is to create a transfer object:

Type TTransfer = class; procedure the_event; TData: the_data; .....

running in the WorkerThread:

Transfer := TTransfer.Create(...)
Transfer.the_data. ..... := ....
queue(@Transfer.the_event);

(no Transfer.Free in the thread !!!)

running in the MainThread:

procedure TTransfer.the_event()
begin
...
fetch something from the_data
...
Free; (this is Actually Self.Free; )
end;

-Michael

Reply by email

Lukasz Sokol
to Lukasz Sokol: I did now, and also, beforehands I did not have the TSomeObjectNotifyEvent() type of…
Lukasz Sokol wrote 35 lines
On 19/06/17 00:30, José Mejuto via Lazarus wrote: > El 18/06/2017 a las 22:44, el es via Lazarus escribió: > >> Hence the object, would have to have ITS callback routine be something like a TNotifyEvent ( procedure(AObject: TSomeObject) of object, or something), and the flag set is set when the callback returns to the object context like >> >> procedure TSomeObject.Callback; >> // this procedure is enQueue()d by the thread >> begin >> if Assigned({Self.}FSomeObjectNotifyEvent) then >> FSomeObjectNotifyEvent(Self); > > Hello, > > Just exit the procedure and there is not free problem. You will get an exception when you use object data, not object code. > I did now, and also, beforehands I did not have the TSomeObjectNotifyEvent() type of callback... so in the callback I had to browse through the list of TSomeObject's, searching for the right one - and that was really risky; > > procedure TSomeObject.Callback; > // this procedure is enQueue()d by the thread > begin > if Assigned({Self.}FSomeObjectNotifyEvent) then > FSomeObjectNotifyEvent(Self); > Self.CanBeDestroyed := true; > Exit; > end; > > After you set CanBeDestroyed to true you must consider all data as RANDOM, so don't access any property or variable. > Understood, I ensured there is nothing accessing the TSomeObject sent as Self to the event handler, after it comes back.
I also had dual-usage of the TSomeObjects: sometimes, they were supposed to be 'just' processed by the thread and then discarded,
some were supposed to be with callback to the main thread;
But I was still hitting the F0F0F0F0 when the above was being queued, when FSomeObjectNotifyEvent was nil;

So actually, I had to move the (equivalent of)

SomeObject.CanBeDestroyed := true;

to be the very last call of the FSomeObjectNotifyEvent(Self) handler, in the main thread, to handle the ones with callback (enclosed in finally...end),

and in the worker thread, to only Queue() the callbacks if the handler is subscribed to:

if Assigned(SomeObject.FSomeObjectNotifyEvent) then
  Queue(SomeObject.FSomeObjectNotifyEvent);
Thanks, -L.

Reply by email