5

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.

Community
  • 1
  • 1
Redwood
  • 66,744
  • 41
  • 126
  • 187
  • 1
    I'm interested in what the guarantees are as well. I've never seen something like your example cause a problem; however, I do know that acquiring resources that are tied to thread identity (in our case a mutex) have caused my team problems in the past. – Mike Zboray Aug 25 '13 at 20:08
  • 2
    I've asked the same question before. I'll dig it out. – spender Aug 25 '13 at 21:07
  • 1
    Here you are (marking as dupe): [Calling ServiceBase.OnStart and OnStop... same instance?](http://stackoverflow.com/questions/10799937/calling-servicebase-onstart-and-onstop-same-instance) – spender Aug 25 '13 at 21:09
  • 1
    @spender As I understand your question it's asking whether or not the start/stop methods are called on the same instance of the service. That's not what I'm asking about. I'm assuming that both methods are called on the same instance and going beyond that to ask a specific question about start/stop and assignment visibility across start/stop. – Redwood Aug 25 '13 at 21:20
  • 1
    @LawrenceJohnston: It's the same thing. If it's the same instance, then there would be something seriously wrong if stuff you'd previously assigned on it were mysteriously re-assigned by the forces of evil. In short, the answers to my question implicitly confirm that your assignments made in `OnStart` will still be there at `OnStop`. I'd stop using .Net if this wasn't the case because it would be like programming on quicksand. – spender Aug 25 '13 at 21:47
  • 1
    @spender That's not how C# behaves once multiple threads are involved. The compiler is permitted to optimize based on the assumption that only a single thread is involved in, e.g., reading and writing a variable and once multiple threads are involved issues can occur unless proper precautions are taken (using locking, Interlocked.XXX, volatile, etc.). (cont...) – Redwood Aug 26 '13 at 16:10
  • 1
    @spender Without locking or similar assignments made on one thread may not be immediately visible on another. See a working example of an assignment not being visible to another thread in this answer: http://stackoverflow.com/a/1284007/1512 and an excellent rough overview of how modern memory models work at Eric Lippert's blog: http://blogs.msdn.com/b/ericlippert/archive/2011/06/16/atomicity-volatility-and-immutability-are-different-part-three.aspx – Redwood Aug 26 '13 at 16:12

2 Answers2

2

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.

J0e3gan
  • 8,740
  • 10
  • 53
  • 80
0

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.

Pedro.The.Kid
  • 1,968
  • 1
  • 14
  • 18
  • "if the stop event is called before the `new` finishes it can happen that `_foo` is null;" Not true per the example in my answer. "and also `_foo` can be released in [another] place in code so [it's] a good practice to check first." True. – J0e3gan Oct 22 '13 at 14:59
  • @J0e3gan nice spot I assumed the OnStart an OnStop could be called from a different place but yes the SCM dos not let you do it randomly. – Pedro.The.Kid Oct 22 '13 at 15:04