2

I have a service where in the main thread I store some data and read it sometimes from the child thread. With Delphi 7 everything worked fine. Service execute, child thread create, main thread made the data, child thread called Synchronise to get it ... and waited until main thread ServiceThread.ProcessRequests(True);

Now with Delphi 10.3 it seems that Synchronise is not waiting for the main thread to get to the ProcessRequests (idle) ... it calls in the middle of the main Execute processing.

main service thread:

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.SvcMgr, Vcl.Dialogs;

type
  TTestserv2 = class(TService)
    procedure ServiceExecute(Sender: TService);
  private
    { Private declarations }
    procedure log(msg: String);
  public
    function GetServiceController: TServiceController; override;
    function getArrayItem(i: integer): string;
    { Public declarations }
 protected
    function DoCustomControl(CtrlCode: Cardinal): Boolean; override;
  end;

Const
   SERVICE_CONTROL_MyMSG  = 10;

var
  Testserv2: TTestserv2;


implementation

{$R *.dfm}

Uses unit2;

Var
   array1 : Array of string;
   Thread1 : T_Thread1;

procedure ServiceController(CtrlCode: DWord); stdcall;
begin
  Testserv2.Controller(CtrlCode);
end;

function TTestserv2.GetServiceController: TServiceController;
begin
  Result := ServiceController;
end;

procedure TTestserv2.log(msg: String);
Var
   F:TextFile;
   LogFile:String;
   TmpStr:String;
begin
   try
      LogFile := 'c:\testlog1.txt';
      AssignFile(F, LogFile);
      If FileExists(LogFile) then
         Append(F)
         Else
      Rewrite(F);

      DateTimeToString(TmpStr,'yyyy.mm.dd. hh:nn:ss',now);
      WriteLN(F,TmpStr+' - '+Msg);

      Flush(F);
   Finally
      CloseFile(F);
   End;
end;


function TTestserv2.DoCustomControl(CtrlCode: Cardinal): Boolean;
begin
  result := true;
  case CtrlCode of
    SERVICE_CONTROL_MyMSG : log('MyMSG'); 
  end;
end;

procedure TTestserv2.ServiceExecute(Sender: TService);
var
   Msg: String;
   i: integer;
   s: string;
Begin
   Log('Service Execute');
   SetLength(array1, 20);

   Thread1 := T_Thread1.Create;
   Thread1.Priority:=tpNormal;
   Thread1.Resume;
   Log('Thread1 created');

   // Where the magic happens
   for i := 0 to 21 do
   Begin
      s := 'value='+ IntToStr( i*2);
      array1[i] := s;
      Log( IntToStr(i) + '-' + s);
      sleep(100);  // in real code some idSNMP query here
   End;

   while not Terminated  do
   begin
      Sleep(50);
      Log('Service Execute  OK ');
      If Terminated then
         Log('Terminated');
      ServiceThread.ProcessRequests(True);
   end;
End;

function TTestserv2.getArrayItem(i:integer):string;
Begin
   result := array1[i];
End;


end.

Child thread:

unit unit2;

interface

uses
  Windows, Classes, SysUtils, ExtCtrls, SyncObjs, ADODB, ActiveX, Unit1;


type
  T_Thread1 = class(TThread)
  private
    { Private declarations }
      FWakeupEvent   : TSimpleEvent;

      procedure Log(Msg:String);
      procedure Terminate1(Sender: TObject);
      Procedure getdataproc;
  protected
      procedure Execute; override;
  public
      constructor Create;
      Destructor Destroy; override;
  end;

implementation

{ T_Thread1 }

constructor T_Thread1.Create;
begin
   inherited Create(True);
   OnTerminate := Terminate1;
   FreeOnTerminate := False;
End;

procedure T_Thread1.Terminate1(Sender: TObject);
Var
   s2:String;
begin
   CoUninitialize;
End;

Destructor T_Thread1.Destroy;
Begin
   If not Terminated Then Terminate;
   inherited;
End;


procedure T_Thread1.log(msg: String);
Var
   F:TextFile;
   LogFile:String;
   TmpStr:String;
begin
   try
      LogFile := 'c:\testlog2.txt';
      AssignFile(F, LogFile);
      If FileExists(LogFile) then
         Append(F)
         Else
         Rewrite(F);

      DateTimeToString(TmpStr,'hh:nn:ss',now);
      WriteLN(F,TmpStr+' - '+Msg);

      Flush(F);
   Finally
      CloseFile(F);
   End;
end;



procedure T_Thread1.Execute;
Var
   WaitStatus: Cardinal;
begin
   LOG('Execute Start');

   CoInitialize(nil);
   FWakeupEvent := TSimpleEvent.Create;

   repeat
      WaitStatus := WaitForSingleObject(FWakeupEvent.Handle, 1000);

      case WaitStatus of
            WAIT_OBJECT_0: Break;
            WAIT_TIMEOUT:
            Begin
               Log('Timeout');
               Synchronize(getdataproc);
            End;
      Else Break;

      end;
   until (Terminated);

   FreeAndNil(FWakeupEvent);
end;



Procedure T_Thread1.getdataproc;
Var
   i:integer;
   res:string;
Begin
   for i := 0 to 21 do
   Begin
      res := Testserv2.getArrayItem(i);
      log(IntToStr(i)+ '-' + res);
   End;
End;

end.

and the result

log1 for main:

    16:27:01 - Service Execute
    16:27:01 - Thread1 created
    16:27:01 - 0-value=0
    16:27:01 - 1-value=2
    16:27:01 - 2-value=4
    16:27:01 - 3-value=6
    16:27:01 - 4-value=8
    16:27:01 - 5-value=10
    16:27:01 - 6-value=12
    16:27:02 - 7-value=14
    16:27:02 - 8-value=16
    16:27:02 - 9-value=18
    16:27:02 - 10-value=20
    16:27:02 - 11-value=22
    16:27:02 - 12-value=24
    16:27:02 - 13-value=26
    16:27:02 - 14-value=28
    16:27:02 - 15-value=30
    16:27:03 - 16-value=32
    16:27:03 - 17-value=34
    16:27:03 - 18-value=36
    16:27:03 - 19-value=38
    16:27:03 - 20-value=40
    16:27:03 - 21-value=42
    16:27:03 - Service Execute  OK 

log2 for child thread:

    16:27:01 - Execute Start
    16:27:02 - Timeout
    16:27:02 - 0-value=0
    16:27:02 - 1-value=2
    16:27:02 - 2-value=4
    16:27:02 - 3-value=6
    16:27:02 - 4-value=8
    16:27:02 - 5-value=10
    16:27:02 - 6-value=12
    16:27:02 - 7-value=14
    16:27:02 - 8-value=16
    16:27:02 - 9-value=18
    16:27:02 - 10-
    16:27:02 - 11-
    16:27:02 - 12-
    16:27:02 - 13-
    16:27:02 - 14-
    16:27:02 - 15-
    16:27:02 - 16-
    16:27:02 - 17-
    16:27:02 - 18-
    16:27:02 - 19-
    16:27:02 - 20-
    16:27:02 - 21-
    16:27:03 - Timeout
    16:27:03 - 0-value=0
    16:27:03 - 1-value=2
    16:27:03 - 2-value=4
    16:27:03 - 3-value=6
    16:27:03 - 4-value=8
    16:27:03 - 5-value=10
    16:27:03 - 6-value=12
    16:27:03 - 7-value=14
    16:27:03 - 8-value=16
    16:27:03 - 9-value=18
    16:27:03 - 10-value=20
    16:27:03 - 11-value=22
    16:27:03 - 12-value=24
    16:27:03 - 13-value=26
    16:27:03 - 14-value=28
    16:27:03 - 15-value=30
    16:27:03 - 16-value=32
    16:27:03 - 17-value=34
    16:27:03 - 18-value=36
    16:27:03 - 19-
    16:27:03 - 20-
    16:27:03 - 21-
    16:27:04 - Timeout
    16:27:04 - 0-value=0
    16:27:04 - 1-value=2
    16:27:04 - 2-value=4
    16:27:04 - 3-value=6
    16:27:04 - 4-value=8
    16:27:04 - 5-value=10
    16:27:04 - 6-value=12
    16:27:04 - 7-value=14
    16:27:04 - 8-value=16
    16:27:04 - 9-value=18
    16:27:04 - 10-value=20
    16:27:04 - 11-value=22
    16:27:04 - 12-value=24
    16:27:04 - 13-value=26
    16:27:04 - 14-value=28
    16:27:04 - 15-value=30
    16:27:04 - 16-value=32
    16:27:04 - 17-value=34
    16:27:04 - 18-value=36
    16:27:04 - 19-value=38
    16:27:04 - 20-value=40
    16:27:04 - 21-value=42

So for the first two round the child calls in the middle of the for cycle of the main.

Does not wait. In real code array is an array of records with more string and integer items.

Sometimes (very very rare) result is like this: ???†??????e se OK ?ô Like Synchronise is not working properly. (compiled to 32 and 64 bit, same result)

What can I do? Not thrust Synchronise ? Criticalsection ?

Do not want to rewrite everything. The child PostThreadMessage CM_SERVICE_CONTROL_CODE to main, and main PostThreadMessage back with a bit more data (some kB) ... I try to avoid.

Any suggestions ?

Ken White
  • 123,280
  • 14
  • 225
  • 444
MrZed
  • 85
  • 5
  • IIRC Synchronize() needs a message loop, so is to be used only with GUI apps (VCL/FMX). It is likely not to be used within a service. – Arnaud Bouchez Aug 24 '20 at 17:26
  • The service main thread by default has a message queue. I send regularly messages to it (with CM_SERVICE_CONTROL_CODE). But, the child thread (calling the Synchronise ) in this example does not have a message queue. Does it need one ? – MrZed Aug 24 '20 at 17:34
  • 1
    @ArnaudBouchez `TThread.Synchronize()` is handled by the main message loop in `TServiceApplication.Run()` in the project's `.dpr` file. It works just fine in a service, but it is just not used with the thread that the `TService` is actually running in, which is NOT the actual main thread. – Remy Lebeau Aug 24 '20 at 17:42
  • @MrZed no, the child thread does not need its own message queue, unless you want to post messages back to the child thread. – Remy Lebeau Aug 24 '20 at 17:44

1 Answers1

2

The TService.OnExecute event is NOT fired in the actual main thread! It is fired in a worker thread that is created by the main thread. The main message loop that handles TThread.Synchronize() requests is in the project's .dpr file where TServiceApplication.Run() is called.

In a typical TService project, there are at least 3 threads running by default:

  • the project main thread, which handles the main message loop, and fires each TService's (Before|After)Install and (Before|After)Uninstall events if needed.

  • the StartServiceCtrlDispatcher() thread, which maintains a connection to the SCM, and dispatches SCM requests to each TService.Controller callback.

  • a thread for each TService, which fires that service's On(Start|Stop|Shutdown), On(Pause|Continue), and OnExecute events based on SCM requests received by the StartServiceCtrlDispatcher() thread.

When your OnExecute event handler calls ServiceThread.ProcessRequests(), it is handling pending SCM requests - in the form of CM_SERVICE_CONTROL_CODE messages that are posted to the TService's thread from the TService.Controller callback function, which is called by StartServiceCtrlDispatcher() in a worker thread that is created by the main thread. It is NOT handling pending Synchronize() requests at all.

So, your 2 threads are NOT synchronizing with each other at all. You need to re-think your synchronization logic. If you want your T_Thread1 to sync with your TTestserv2, then one option would be to have TTestserv2 create a hidden HWND for itself (such as with System.Classes.AllocateHWnd()) and then T_Thread1 can send/post window messages to that HWND as needed. Calling ProcessRequests() in the OnExecute event (in TTestserv2's thread) will dispatch those window messages as needed.

Also, speaking of ProcessRequests(), know that calling ProcessRequests() with WaitForMessage=True will block the calling thread until the service is terminated, processing all SCM requests (and window messages) internally as needed. If you want your OnExecute event handler to run its own loop, you need to call ProcessRequests() with WaitForMessage=False instead.

And FYI, everything I have said applies to Delphi 7, too.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Hmmm, trying to digest it :) Before application.run (in dpr) I ask the GetCurrentThreadId then I got a different handle as when I ask it in the service execute (unit1). The messages I send to the service (with CM_SERVICE_CONTROL_CODE) I send to the unit1 handle, not the dpr handle. That's confusing :) So I should make a new handle in the unit1 execute and a message queue to it ... and send postmessage to this new handle (and Synchronise will use that) ? Will try, but my head hurts :D And yep, it worked in D7. Some of my services running for more than 8 years continuously. – MrZed Aug 24 '20 at 18:29
  • @MrZed "*Before application.run (in dpr) I ask the GetCurrentThreadId then I got a different handle as when I ask it in the service execute (unit1)*" - yes, because they are different threads. "*The messages I send to the service (with CM_SERVICE_CONTROL_CODE) I send to the unit1 handle, not the dpr handle*" - you have not shown any of that code yet. But there is no "unit1 handle", do you mean `TService.ServiceThread.Handle`? You can't use `PostMessage()` with that `Handle`. But you can use `PostThreadMessage()` with `TService.ServiceThread.ThreadID` instead. – Remy Lebeau Aug 24 '20 at 18:36
  • @MrZed "*I should make a new handle in the unit1 execute and a message queue to it and send postmessage to this new handle*" - the `TService` thread already has a message queue, to handle SCM request messages. But yes, `OnExecute` could create an `HWND` that you can then post/send custom messages to, yes. That is *one* option (there are others). "*(and Synchronise will use that) ?*" - no, that is not what I said. This approach would be *INSTEAD OF* using `Synchronize()` at all. – Remy Lebeau Aug 24 '20 at 18:37
  • @MrZed "*And yep, it worked in D7. Some of my services running for more than 8 years continuously*" - this code is BROKEN in D7 as well as in 10.3. The fact that it "works" at all is a **fluke**, you were simply *lucky* this hadn't gone sour before. You need to fix the root issue, which is bad synchronization between threads. – Remy Lebeau Aug 24 '20 at 18:38
  • True, I use PostThreadMessage with ServiceThread.ThreadID to send messages to main worker (unit1). So if I understand you correctly I can not use Synchronise (in child threads) to service main worker thread (unit1), no way. I have to use messages. And if I need only to signal unit1, to PostThreadMessage data to unit2, then I do not need the new HWND. Like now I can PostThreadMessage CM_SERVICE_CONTROL_CODE with own wParam. ( TTestserv2 = class(TService) protected function DoCustomControl(CtrlCode: Cardinal): Boolean; override; ... and in it I receive wParam ). And thank for the help – MrZed Aug 24 '20 at 19:22
  • @MrZed "*I use PostThreadMessage with ServiceThread.ThreadID*" - you STILL have not shown that code, so we can't see if you use it correctly. "*So if I understand you correctly I can not use Synchronise*" - I did not say that. You CAN use it, but your service thread would ALSO have to use it too, so that BOTH threads sync with each other by both going through the *real* main thread. If you use `DoCustomControl()` instead, just note that you can only use it as a signal, you can't pass any data in it, you still need a proper data sync between your threads, even if just a CriticalSection. – Remy Lebeau Aug 24 '20 at 19:28
  • It is all in the prev msg. A new protected function to TTestserv2. And the other side is simple too: Const SERVICE_CONTROL_MyMSG = 10; function TTestserv2.DoCustomControl(CtrlCode: Cardinal): Boolean; begin result := true; case CtrlCode of SERVICE_CONTROL_MyMSG : log('MyMSG'); end; end; Using it like from D3. Instead of log I can send back the data with PostThreadMessage to unit2 – MrZed Aug 24 '20 at 19:32
  • 1
    @MrZed "*It is all in the prev msg*" - that is not good enough. You need to edit your question to show the ACTUAL CODE that is calling `PostThreadMessage()`, and the ACTUAL CODE that is processing the posted message. Showing us only the *declaration* of `DoCustomControl()`, and only a *description* of your handling, is not helpful. We need to see the CODE. – Remy Lebeau Aug 24 '20 at 19:33
  • _so that BOTH threads sync with each other by both going through the real main thread_ OK I think the solution is to move everything from unit1 execute to a new there created thread (unit0) ... and that creates unit2 like unit1 did before ... and I create a message queue in unit0 and Synchronise will work from unit2 to unit0. Testing ... – MrZed Aug 24 '20 at 19:55
  • Thanks for the help! As you wrote, Synchronise do not work in ServiceExecute. Accepted. – MrZed Aug 26 '20 at 16:03