views:

1108

answers:

5

I have created a Windows Service that will be calling out to some COM components, so I tagged [STAThread] to the Main function. However, when the timer fires, it reports MTA and the COM calls fail. How can I fix this?

using System;
using System.Diagnostics;
using System.ServiceProcess;
using System.Threading;
using System.Timers;



namespace MyMonitorService
{
    public class MyMonitor : ServiceBase
    {
        #region Members
        private System.Timers.Timer timer = new System.Timers.Timer();
        #endregion

        #region Construction
        public MyMonitor ()
        {
            this.timer.Interval = 10000; // set for 10 seconds
            this.timer.Elapsed += new System.Timers.ElapsedEventHandler(this.timer_Elapsed);
        }
        #endregion

        private void timer_Elapsed (object sender, ElapsedEventArgs e)
        {
            EventLog.WriteEntry("MyMonitor", String.Format("Thread Model: {0}", Thread.CurrentThread.GetApartmentState().ToString()), EventLogEntryType.Information);
        }

        #region Service Start/Stop
        [STAThread]
        public static void Main ()
        {
            ServiceBase.Run(new MyMonitor());
        }

        protected override void OnStart (string[] args)
        {
            EventLog.WriteEntry("MyMonitor", "My Monitor Service Started", EventLogEntryType.Information);
            this.timer.Enabled = true;
        }

        protected override void OnStop ()
        {
            EventLog.WriteEntry("MyMonitor", "My Monitor Service Stopped", EventLogEntryType.Information);
            this.timer.Enabled = false;
        }
        #endregion
    }
}
+2  A: 

That cannot work in a service, the thread that calls your Main() method was already started by the service manager. You'll need to create a separate thread that is initialized with Thread.SetApartmentState() and pumps a message loop.

Hans Passant
I think the problem lies within the threading apartment of the timer thread because I get the same problem in a console application (Main thread is STA, but in timer event the threading apartment is MTA).
0xA3
Yeah - the timer is going to be an MTA thread. I addressed this specifically in my answer.
Reed Copsey
A: 

Looking at a similar example: http://www.aspfree.com/c/a/C-Sharp/Creating-a-Windows-Service-with-C-Sharp-introduction/1/

What if your main is...

    [STAThread]
    public static void Main ()
    {
        MyMonitor m = new MyMonitor();
        m.Start();
    }

and move your timer start / stop out of the events...

 public void Start() { this.timer.Enabled = true;}
 public void Stop() { this.timer.Enabled = false;}

  protected override void OnStart (string[] args)
    {
        EventLog.WriteEntry("MyMonitor", "My Monitor Service Started", EventLogEntryType.Information);
    }

    protected override void OnStop ()
    {
        EventLog.WriteEntry("MyMonitor", "My Monitor Service Stopped", EventLogEntryType.Information);
    }
Tim Hoolihan
+3  A: 

Setting the STAThread attribute will not work on a service. It's not being handled the same way as an application, so this will get ignored.

My recommendation would be to manually make a separate thread for your service, set its apartment state, and move everything into it. This way, you can set the thread to STA correctly.

However, there will be another issue here - you'll have to rework the way your service works. You can't just use a System.Threading.Timer instance for timing - it runs on a separate thread, which will not be STA. When its elapsed event fires, you'll be working on a different, non-STA thread.

Instead of doing your work in the timer event, you'll probably want to do your main work in the thread you create explicitly. You can have a reset event in that thread which blocks, and have your timer "set" it to allow your logic to run in the STA thread.

Reed Copsey
There are plenty of examples on the web using STAThread on the main of a service. Are they wrong?
Tim Hoolihan
Most of them don't work, though. The service is started by the service manager, in a separate thread, and doesn't use the Main routine the same way as an application.
Reed Copsey
The OP uses a `System.Timers.Timer` (which is still running on an MTA). However, I still don't understand, why STAThread should not work in a service?
0xA3
The Main method immediately returns. All its doing is telling the SCM that it should run a particular service type defined in a particular dll. The SCM then determines when (and if) to run the service using its own threads.
Will
+2  A: 

Services are run by the windows service hosting system, which runs using MTA threads. You can't control this. You have to create a new Thread and set its ApartmentState to STA, and do your work on this thread.

Here's a class that extends ServiceBase that does this:

public partial class Service1 : ServiceBase
{
    private System.Timers.Timer timer;

    public Service1()
    {
        InitializeComponent();
        timer = new System.Timers.Timer();
        this.timer.Interval = 10000; // set for 10 seconds
        this.timer.Elapsed += new System.Timers.ElapsedEventHandler(Tick);
    }

    protected override void OnStart(string[] args)
    {
        timer.Start();
    }

    private void Tick(object sender, ElapsedEventArgs e)
    {
        // create a thread, give it the worker, let it go
        // is collected when done (not IDisposable)
        var thread = new Thread(WorkerMethod);
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
    }

    private void WorkerMethod(object state)
    {
        // do your work here in an STA thread
    }

    protected override void OnStop()
    {
        timer.Stop();
    }
}

Note this code doesn't actually stop the service, it stops the timer. There could be lots of work still being done on multiple threads. For instance, if your work consisted of running multiple queries off a large database you may end up crashing because you have too many threads running at the same time.

In a situation like this, I'd create a set number of STA threads (maybe 2x the number of cores to start off with) which monitor a thread-safe queue for work items. The timer tick event would be responsible for loading that queue with the work needing done.

It all depends on what you're actually doing every ten seconds, whether or not it should be completed the next time the timer ticks, what you should do in this situation, etc etc.

Will
+1 for the workaround; however, how do you explain that the same problem exists for console applications? From my understanding your explanation seems not correct though. A service is started as a child process of the service control manager (and not as a thread) and then the service connects to the controller by calling `StartServiceCtrlDispatcher` (from what I can see in Reflector) so starting a service as STA should be possible.
0xA3
It does not exist for console apps. Console apps respect the STAThreadAttribute. Services do not. The SCM does not run in an STA thread. The SCM does not respect nor does it provide any method for services to communicate their desired apartment state. The OP's code is confusing, as it includes the Main method. All the Main method does is say, "Hey, SCM, here's a DLL containing a service type I want you to run, kthxbai" and goes away. Yeah, that runs in an STA thread, but it does not block and immediately returns. The SCM then determines when to run the service using its own threads.
Will
Will,After looking up some more on the web and your response, I have a new structure below (there wasn't enough characters to leave it as a comment)
Walter Williams
@Will: No, a (.NET) service is an executable and not a dll. As `ServiceBase.Run` calls `StartServiceCtrlDispatcher` under the hood, it should "not return until all running services in the process have entered the SERVICE_STOPPED state" (see http://msdn.microsoft.com/en-us/library/ms686324%28VS.85%29.aspx).
0xA3
Aaah, divo.... I understand your confusion. There are two different types of Timers in the framework--ones that use the ThreadPool and ones that send a windows message to the UI thread. ThreadPool timers (such as System.Threading.Timer) queue work on the ThreadPool, which contains ONLY MTA THREADS. UI thread timers such as System.Windows.Forms.Timer send a message to the main window of the app that says "hey, run this method!", and so this runs in an STA thread (use outside of forms apps is not advised!).
Will
Nitpick: There are actually *three* timers in the BCL (see the paragraph "What is a timer" here: http://www.codeguru.com/columns/dotnet/article.php/c6919/) and I'm not confused, I just wonder why a service should not be STA.
0xA3
(continued) If thread A is STA, and queues a method on the ThreadPool, the method will be run on an MTA thread. Always. Period. Full stop. Different thread running the method, see? If this is confusing, pls grab a copy of CLR Via C#. Also, "a (.NET) is an executable and not a dll" makes little sense to me. And your link is not appropriate to the subject under discussion. See http://msdn.microsoft.com/en-us/library/tks2stkt.aspx
Will
(continued) You can register any assembly (.exe or .dll) as a service, and this must be done before you call ServiceBase.Run. Trust me on that one, I've written a few that are in production. VS comes with InstallUtil http://msdn.microsoft.com/en-us/library/sd8zc8ha(VS.80).aspx but installers will also do this as well.
Will
@Will: Well, if you look with Reflector you will see that ServiceBase.Run calls the method I previously mentioned. And a service is very well a separate executable running in its own process. It is not a dll loaded into the service controller. It's just controlled by the SCM using the connection established via the StartServiceCtrlDispatcher function. You can as well run a dll as a service, but that dll will then be hosted in a generic process (svchost.exe).
0xA3
(continued) Services aren't executing in an STA thread because that's how the SCM is written. It loads your service assembly, instantiates your service type and calls Start() on it. The SCM does this using its own threads, which are MTA threads. That's. Just. The. Way. It. Is. In the long run, it doesn't really matter as services MUST implement their own threading models. You can't do all your work in the Start method; the SCM will kill your service as Start does not return. You have to create a Thread or a Timer or whatever that runs the code your service needs to run.
Will
Dude, you're splitting hairs. I can do it too--there are actually MORE than three Timers. You're missing the DispatcherTimer. Nyah.
Will
@walter just a quick note this sample code is a quick hack. I wouldn't suggest creating a new Thread in each Tick event. Wanted to be sure you understood that.
Will
A: 

This reports that it is using STA. It is based on Will's suggestion and http://en.csharp-online.net/Creating_a_.NET_Windows_Service%E2%80%94Alternative_1:_Use_a_Separate_Thread

using System;
using System.Diagnostics;
using System.ServiceProcess;
using System.Threading;



namespace MyMonitorService
{
    internal class MyMonitorThreaded : ServiceBase
    {
        private Boolean bServiceStarted = false;
        private Thread threadWorker;

        private void WorkLoop ()
        {
            while (this.bServiceStarted)
            {
                EventLog.WriteEntry("MyMonitor", String.Format("Thread Model: {0}", Thread.CurrentThread.GetApartmentState().ToString()), EventLogEntryType.Information);

                if (this.bServiceStarted)
                    Thread.Sleep(new TimeSpan(0, 0, 10));
            }

            Thread.CurrentThread.Abort();
        }

        #region Service Start/Stop
        protected override void OnStart (String[] args)
        {
            this.threadWorker = new Thread(WorkLoop);
            this.threadWorker.SetApartmentState(ApartmentState.STA);
            this.bServiceStarted = true;
            this.threadWorker.Start();
        }

        protected override void OnStop ()
        {
            this.bServiceStarted = false;
            this.threadWorker.Join(new TimeSpan(0, 2, 0));
        }
        #endregion
    }
}
Walter Williams