Redwood
Redwood

Reputation: 69292

Do Windows services need to ensure that commands can be processed on different threads?

When run via the Service Control Manager do Windows services need to assume that command processing methods (OnStart, OnStop, etc.) can be called on different threads with nothing ensuring that, e.g., assignments to members will be visible between methods?

public class MyService : ServiceBase {

    private Object _foo;    

    protected override void OnStart(string[] args) {
        _foo = new Object();
    }

    protected override void OnStop() {
        if (_foo == null) {
            throw new Exception("Assignment not visible"); // Can this happen?
        }

        _foo = null;
    }

}

I can't find a guarantee that the exception in my example won't be thrown, but all of the examples I've found, including elsewhere on StackOverflow, seems to assume that, e.g., assignments to variables in OnStart() will always be visible in OnStop().

If no such guarantee is made by the SCM I do know how to ensure the assignment is visible (e.g. by adding a lock around all reads/writes in the service). I'm interested in whether or not such measures are necessary.

Upvotes: 5

Views: 347

Answers (2)

J0e3gan
J0e3gan

Reputation: 8938

In one sense, the SCM cannot guarantee that the exception you outline will not be thrown. It does not control the service's manipulation of a private member of course - e.g. if additional service code affects _foo.

Having said this, consider the following scenario to see why the answer to your specific question is clearly no:

1) Build your service with the following changes to demonstrate:

    public partial class MyService : ServiceBase
    {
        private Object _foo;
        private const string _logName = "MyService Log.txt"; // to support added logging

        public MyService()
        {
            InitializeComponent();
        }

        protected override void OnStart(string[] args)
        {
            // demonstrative logging
            var threadId = Thread.CurrentThread.ManagedThreadId;
            using (var log = new StreamWriter(AppDomain.CurrentDomain.BaseDirectory + _logName, true))
            {
                log.WriteLine("{0}:  In OnStart(string[]) on thread ID {1}.  Sleeping for 10 seconds...", DateTime.Now, threadId);
            }

            // Sleep before initializing _foo to allow calling OnStop before OnStart completes unless the SCM synchronizes calls to the methods.
            Thread.Sleep(10000);

            _foo = new Object();
        }

        protected override void OnStop()
        {
            // demonstrative logging added
            var threadId = Thread.CurrentThread.ManagedThreadId;
            using (var log = new StreamWriter(AppDomain.CurrentDomain.BaseDirectory + _logName, true))
            {
                log.WriteLine("{0}:  In OnStop() on thread ID {1}.", DateTime.Now, threadId);
            }

            if (_foo == null)
            {
                // demonstrative logging added
                using (var log = new StreamWriter(AppDomain.CurrentDomain.BaseDirectory + _logName, true))
                {
                    log.WriteLine("{0}:  _foo == null", DateTime.Now);
                }

                throw new Exception("Assignment not visible"); // Can this happen?
            }

            _foo = null;
        }
    }

2) Open a command shell.

3) Open another command shell.

4) In the first command shell, install the service (with sc create) if you haven't already, and start it (with net start). You should see:

The MyService service is starting.....

The trailing dots should be added one by one as the SCM waits through the 10 seconds of sleeping to start the service.

5) In the second command shell, try to stop the service (with net stop) before 10 seconds elapse. You should see:

The service is starting or stopping. Please try again later.

So starting a service is clearly a blocking operation that must complete before the service can be stopped.

6) Check the first command shell after 10 seconds have elapsed. You should see:

The MyService service was started successfully.

7) Return to the second command shell and try to stop the service again. You should see:

The MyService service is stopping.

The MyService service was stopped successfully.

8) Review the resulting log - for example:

10/22/2013 7:28:55 AM: In OnStart(string[]) on thread ID 4. Sleeping for 10 seconds...

10/22/2013 7:29:17 AM: In OnStop() on thread ID 5.

Starting and stopping the service quickly is easier with two command shells I think; but the example works similarly with one command shell too.

Lastly, you might find Mitchell Taylor (CoolDadTx)'s answer to a similar question in MSDN forums interesting as I did:

The threading model used by the SCM isn't formally documented AFAIK. What is known is that each service gets called on its own thread. However the SCM might or might not use a thread pool to reuse a thread across services. When a service is called (start, stop, custom commands, etc) it is expected to perform its task and return quickly. There is a strong limit on how long it can take. Anything more than a quick return requires that you push the request to a secondary thread for processing. The SCM itself runs on a separate thread so if a service takes too long to respond then the SCM sees it as hung. This is discussed here: http://msdn.microsoft.com/en-us/library/ms684264(VS.85).aspx

UPDATE:

Particularly note the Service State Transitions MSDN article to which the article that Mitchell Taylor cites links. It contains a state diagram that pretty clearly & authoritatively documents defined service state transitions and aligns with what I outlined above. It also explains in relation to the state diagram how the SCM does not transmit service control requests at times to ensure only defined state transitions.

Upvotes: 2

Pedro.The.Kid
Pedro.The.Kid

Reputation: 2078

the example you gave can in did happen its just very very improbable ex:

_foo = new VeryLongAndTimeConsummingTask();

[EDIT]: as referred in comment SCM prevents the OnStop from Running before the OnStart completes. this comment comes from my possible bad practice of exposing the start and stop wrappers as public.

if the stop event is called before the new finishes it can happen that the _foo is null;

and also _foo can be released in adder place in code so its a good practice to check first.

Upvotes: 0

Related Questions