Tuesday, July 18, 2006

Trouble with services

Multithreaded programming is a pain in the <insert body part>. Whatever you do, sooner or later your code will stop working correctly.

Service writing is not simple, either. There are many limitations that should be taken into account (lack of graphical user interface, for example).

For quite some time now, I was sure I mastered them both, but not so long ago a service/thread combination proved me wrong. There's a catch in service programming that I was not aware of until I had to spend several days tracing down one rarely occurring problem. It was so rare that it happened only at one customer and even there the application crashed only once in several hours.

So what's the catch? It is quite simple. Every Windows service is multithreaded. Even if you don't create threads in the service application, it will be using two threads.

It gets even worse. On the service form, OnCreate and OnDestroy are called from the main application thread while OnStart and OnStop are called from the main service thread.

There is a logic behind this, and it is quite obvious once you stop to think about it. A service application can contain several servicesand as they are independent entities, each must run in a separate thread. On the other hand, OnCreate/OnDestroy are called from the main application which creates those services, and are therefore called from the main application thread. Simple.

But it can bite you.

If you drop some components on the service form, they will be created in destroyed from the main application thread. Of course, they will be used from the appropriate service thread (or from the service thread, if there's only one service in the application, which is usually the case).

Usually, you won't notice this. But things start getting weird if component in question creates hidden message-processing window. Main application thread will process messages for this hidden window, but you'll usually want to process them in the service thread. Even worse, if you implement some kind of message pump in the service thread, two threads will be processing messages for the same component. It won't happen frequently, but it surely will happen and you can imagine the result. Deadlocks. Data corruption. Mayhem.

Yes, that's what happened to me.

BTW, that's also why TTimer doesn't work correctly if you drop it on the service form.

If you don't believe me, try it for yourself. Create a new service application and add some logging to the OnCreate/OnStart/OnStop/OnDestroy:

procedure TService1.ServiceDestroy(Sender: TObject);
begin
Log(Format('Destroy called from process %d thread %d',
[GetCurrentProcessID, GetCurrentThreadID]));
end;

procedure TService1.ServiceCreate(Sender: TObject);
begin
Log(Format('Create called from process %d thread %d',
[GetCurrentProcessID, GetCurrentThreadID]));
end;

procedure TService1.ServiceStart(Sender: TService; var Started: boolean);
begin
Log(Format('Start called from process %d thread %d',
[GetCurrentProcessID, GetCurrentThreadID]));
Started := true;
end;

procedure TService1.ServiceStop(Sender: TService; var Stopped: boolean);
begin
Log(Format('Stop called from process %d thread %d',
[GetCurrentProcessID, GetCurrentThreadID]));
Stopped := true;
end;

I used following trivial logger in the demo:

procedure Log(const msg: string);
var
fLog: textfile;
begin
AssignFile(fLog, 'c:\ServiceDemo.log');
if FileExists('c:\ServiceDemo.log') then
Append(fLog)
else
Rewrite(fLog);
Writeln(fLog, msg);
CloseFile(fLog);
end;

Compile and run:
  • ServiceDemo /install

  • net start ServiceDemo

  • net stop ServiceDemo

  • ServiceDemo /uninstall


Service will get installed, started, stopped and uninstalled. Now check the log file:
Create called from process 6020 thread 4188
Destroy called from process 6020 thread 4188
Create called from process 4840 thread 3184
Start called from process 4840 thread 4588
Stop called from process 4840 thread 4588
Destroy called from process 4840 thread 3184
Create called from process 2968 thread 4640
Destroy called from process 2968 thread 4640

What happened here? First OnCreate/OnDestroy got called (when ServiceDemo /install was run). Next, OnCreate was called from thread 3184, OnStart/OnStop were called from thread 4588 (I told you!) and OnDestroy was called from thread 3184. Finally OnCreate/OnDestroy were called then ServiceDemo was run with /uninstall switch.

So there's another problem with dropping components on the service form - they will also be created/destroy whenever service application is called with /install or /uninstall switch!

OK, you're asking, but what can we do? I suggest creating components in the OnStart event - either manually (in source) or by dropping them into a separate data module, which is not created automatically but in the OnStart event:

procedure TService1.ServiceStart(Sender: TService; var Started: boolean);
begin
Log(Format('Start called from process %d thread %d',
[GetCurrentProcessID, GetCurrentThreadID]));
FDataModule := TDataModule2.Create(nil);
Started := true;
end;

procedure TService1.ServiceStop(Sender: TService; var Stopped: boolean);
begin
Log(Format('Stop called from process %d thread %d',
[GetCurrentProcessID, GetCurrentThreadID]));
FreeAndNil(FDataModule);
Stopped := true;
end;

(BTW, in Delphi 2006 you can only prevent data module from being created automatically in service application by removing the appropriate CreateForm line in the project source.)

It's obvious that data module's OnCreate is now called in the context of the same thread as services OnStart, but just to be sure we can install the service again and check the log:
Create called from process 2472 thread 4344
Destroy called from process 2472 thread 4344
Create called from process 4824 thread 5216
Start called from process 4824 thread 3144
DM Create called from process 4824 thread 3144
Stop called from process 4824 thread 3144
DM Destroy called from process 4824 thread 3144
Destroy called from process 4824 thread 5216
Create called from process 3588 thread 332
Destroy called from process 3588 thread 332

The moral for today: Dont drop anything on the service form. Ever!

Complete demo project is available at: http://17slon.com/blogs/gabr/files/svc_demo.zip

4 comments:

  1. JP Pienaar11:54

    Hi

    Services made easy = SVCOM
    SVCOM has its own timer etc
    Agreed:Using datamodules that gets created in onstart is the best way.

    In onstart I use:
    CoInitialize(nil);
    CreateForms;

    and in OnStop:
    FreeForms;
    CoUninitialize;

    Using CreateForms to create all DMs
    and FreeForms to free them

    ReplyDelete
  2. It's true, SvCom is great. I'm using it everytime I have to write a service.

    But it exhibits the same "problem" (we've seen that it is really a design decision, not a problem per se) - OnCreate is called from one thread and OnStart from others.

    ReplyDelete
  3. Xepol15:00

    Alternate solution:

    Make your service module ULTRA lightweight. Put all your actual code into a datamodule. Datamodule gets created and destroyed in the OnExecute.

    Do real work in the data module, and voila, problem solved, except for knowing when you have have extra threading issues brought by third party libraries like the BDE.

    Of course, for a long time I just used Peter Sawatzki which does let you use VCL code - it all runs in the primary thread and you just put a lightweight loop in the service thread itself. Sadly, he hasn't maintained it and as delphi and windows evolved, it has started to develop "issues".

    ReplyDelete
  4. That certainly sounds like a good advice - something that I may adopt in the future.

    ReplyDelete