La mejor solución para un servicio de Windows con subprocesos en ejecución constante en C # 4.0

I want to create a windows service that will create x number of threads that wake up every x number of minutes and do some work.

I think the task scheduling or parallel framework is a bad fit for this type of work as it is best suited for work that starts, completes and finishes rather than is constant.

Should I look at utilising a thread pool for this approach or does anyone have any advice for a good solution?

preguntado el 09 de enero de 11 a las 07:01

5 Respuestas

Really, it sounds like you only need one thread.

Here's a helper class that I created for exactly this kind of thing. Here's how you use it:

class MyPeriodicTasks : PeriodicMultiple
{
    // The first task will start 30 seconds after this class is instantiated and started:
    protected override TimeSpan FirstInterval { get { return TimeSpan.FromSeconds(30); } }

    public MyPeriodicTasks()
    {
        Tasks = new[] {
            new Task { Action = task1, MinInterval = TimeSpan.FromMinutes(5) },
            new Task { Action = task2, MinInterval = TimeSpan.FromMinutes(15) },
        };
    }

    private void task1() { /* code that gets executed once every 5 minutes */ }
    private void task2() { /* code that gets executed once every 15 minutes */ }
}

Then, to start the tasks:

var tasks = new MyPeriodicTasks();
tasks.Start();

And during service shutdown:

tasks.Shutdown();

(alternatively, call Start with backgroundThread: true, then you don't need to call Shutdown, but then a task may just get terminated right in the middle of doing something)

Here's the actual code:

/// <summary>
/// Encapsulates a class performing a certain activity periodically, which can be initiated once
/// and then permanently shut down, but not paused/resumed. The class owns its own separate
/// thread, and manages this thread all by itself. The periodic task is executed on this thread.
/// <para>The chief differences to <see cref="System.Threading.Timer"/> are as follows. This
/// class will never issue overlapping activities, even if an activity takes much longer than the interval;
/// the interval is between the end of the previous occurrence of the activity and the start of the next.
/// The activity is executed on a foreground thread (by default), and thus will complete once started,
/// unless a catastrophic abort occurs. When shutting down the activity, it's possible to wait until the
/// last occurrence, if any, has completed fully.</para>
/// </summary>
public abstract class Periodic
{
    private Thread _thread;
    private CancellationTokenSource _cancellation;
    private ManualResetEvent _exited;

    /// <summary>
    /// Override to indicate how long to wait between the call to <see cref="Start"/> and the first occurrence
    /// of the periodic activity.
    /// </summary>
    protected abstract TimeSpan FirstInterval { get; }

    /// <summary>
    /// Override to indicate how long to wait between second and subsequent occurrences of the periodic activity.
    /// </summary>
    protected abstract TimeSpan SubsequentInterval { get; }

    /// <summary>
    /// Override with a method that performs the desired periodic activity. If this method throws an exception
    /// the thread will terminate, but the <see cref="LastActivity"/> will occur nevertheless.
    /// </summary>
    protected abstract void PeriodicActivity();

    /// <summary>
    /// Override with a method that performs an activity on the same thread as <see cref="PeriodicActivity"/> during
    /// shutdown, just before signalling that the shutdown is complete. The default implementation of this method
    /// does nothing. This method is guaranteed to be called during a shutdown, even if the shutdown is due to an
    /// exception propagating outside of <see cref="PeriodicActivity"/>.
    /// </summary>
    protected virtual void LastActivity() { }

    /// <summary>
    /// Returns false before the first call to <see cref="Start"/> and after the first call to <see cref="Shutdown"/>;
    /// true between them.
    /// </summary>
    public bool IsRunning { get { return _cancellation != null && !_cancellation.IsCancellationRequested; } }

    /// <summary>
    /// Schedules the periodic activity to start occurring. This method may only be called once.
    /// </summary>
    /// <param name="backgroundThread">By default (false) the class will use a foreground thread, preventing application shutdown until the thread has terminated. If true, a background thread will be created instead.</param>
    public virtual void Start(bool backgroundThread = false)
    {
        if (_thread != null)
            throw new InvalidOperationException(string.Format("\"Start\" called multiple times ({0})", GetType().Name));

        _exited = new ManualResetEvent(false);
        _cancellation = new CancellationTokenSource();
        _thread = new Thread(threadProc) { IsBackground = backgroundThread };
        _thread.Start();
    }

    private volatile bool _periodicActivityRunning = false;

    /// <summary>
    /// Causes the periodic activity to stop occurring. If called while the activity is being performed,
    /// will wait until the activity has completed before returning. Ensures that <see cref="IsRunning"/>
    /// is false once this method returns.
    /// </summary>
    public virtual bool Shutdown(bool waitForExit)
    {
        if (waitForExit && _periodicActivityRunning && Thread.CurrentThread.ManagedThreadId == _thread.ManagedThreadId)
            throw new InvalidOperationException("Cannot call Shutdown(true) from within PeriodicActivity() on the same thread (this would cause a deadlock).");
        if (_cancellation == null || _cancellation.IsCancellationRequested)
            return false;
        _cancellation.Cancel();
        if (waitForExit)
            _exited.WaitOne();
        return true;
    }

    private void threadProc()
    {
        try
        {
            _cancellation.Token.WaitHandle.WaitOne(FirstInterval);
            while (!_cancellation.IsCancellationRequested)
            {
                _periodicActivityRunning = true;
                PeriodicActivity();
                _periodicActivityRunning = false;
                _cancellation.Token.WaitHandle.WaitOne(SubsequentInterval);
            }
        }
        finally
        {
            try { LastActivity(); }
            finally { _exited.Set(); }
        }
    }
}

/// <summary>
/// <para>Encapsulates a class performing multiple related yet independent tasks on the same thread
/// at a certain minimum interval each. Schedules the activity that is the most late at every opportunity,
/// but will never execute more than one activity at a time (as they all share the same thread).</para>
/// </summary>
public abstract class PeriodicMultiple : Periodic
{
    /// <summary>
    /// Used to define the activities to be executed periodically.
    /// </summary>
    protected sealed class Task
    {
        /// <summary>The activity to be performed.</summary>
        public Action Action;
        /// <summary>The mimimum interval at which this activity should be repeated. May be delayed arbitrarily though.</summary>
        public TimeSpan MinInterval;
        /// <summary>Stores the last time this activity was executed.</summary>
        public DateTime LastExecuted;
        /// <summary>Calculates by how much this activity has been delayed. Is used internally to pick the next activity to run. Returns negative values for activities that aren't due yet.</summary>
        public TimeSpan DelayedBy()
        {
            if (LastExecuted == default(DateTime))
                return TimeSpan.FromDays(1000) - MinInterval; // to run shortest interval first when none of the tasks have ever executed
            else
                return (DateTime.UtcNow - LastExecuted) - MinInterval;
        }
    }

    /// <summary>If desired, override to provide a custom interval at which the scheduler
    /// should re-check whether any activity is due to start. Defaults to 1 second.</summary>
    protected override TimeSpan SubsequentInterval { get { return TimeSpan.FromSeconds(1); } }

    /// <summary>Initialise this with the list of activities to be executed.</summary>
    protected IList<Task> Tasks;

    /// <summary>For internal use.</summary>
    protected sealed override void PeriodicActivity()
    {
        TimeSpan maxDelay = TimeSpan.MinValue;
        Task maxDelayTask = null;

        foreach (var task in Tasks)
        {
            var delayedBy = task.DelayedBy();
            if (maxDelay < delayedBy && delayedBy > TimeSpan.Zero)
            {
                maxDelay = delayedBy;
                maxDelayTask = task;
            }
        }

        if (maxDelayTask != null)
        {
            maxDelayTask.LastExecuted = DateTime.UtcNow;
            maxDelayTask.Action();
        }
    }
}

The thread spends most of the time sleeping, but it does wake up every 1 second to check if a task is due. This 1 second interval is probably too short for intervals like 15 minutes, so reduce it to something like 30 seconds instead (that would be the SubsequentInterval).

¡Espero que sea útil!

Respondido el 09 de enero de 11 a las 16:01

(see the <summary> of Periodic to understand how this differs from the Threading.Timer approach). - Roman Starkov

You could probably just use the Task Parallel Library from MS to do this. - Ryan

I don't think the task library is best suited for these long running tasks. - dagda1

You're seriously over engineering this! All you need is: Observable.Interval(Timespan.FromMinutes(15)).Subscribe(i => CheckForTasksDueAndStartThem());. I use this sort of code in many many enterprise services (doing anywhere from 1 action per day to dozens per second). - Ryan

@Ryan you could argue that I'm reinventing the wheel, but I find the suggestion that it's over-engineered dubious :) What do you want to bet that Observable with all its infrastructure is FAR more code than the above? Having said that, reinventing wheels is also undesirable, and I will read up on using Observable instead. - Roman Starkov

It makes very little sense to start x threads to do x jobs when you intentionally don't let them do any work at all for y minutes. Just have Digital XNUMXk thread do x jobs. It will take x times longer to complete the work (a bit less, actually) but that's no issue at all as long as that takes less than y minutes.

Additional benefits from this is that the service cannot easily impact the responsiveness of the machine, other cores remain available. And that your code becomes a heckofalot easier to implement and debug.

Use the System.Threading.Thread timer to activate the work. The callback runs on a threadpool thread. Starting and stopping the service is easy, just enable/disable that timer.

Respondido el 25 de junio de 12 a las 09:06

Do you really need those threads to run constantly and then wake up after x minutes? I think you may want to consider using an existing scheduler library like Quartz.NET which handle running the tasks for you.

Respondido el 09 de enero de 11 a las 16:01

I have two suggestions for you. First, for building your service, check out TopShelf. It removes all the pain of setting up a Windows service.

Second, you can use the Observable class to create a timer without resorting to writing Timer specific code or Quartz (a pain to configure!).

Aquí hay un código de muestra:

public class MyService
{
    private IDisposable Timer;

    public void Start()
    {
        Timer = ObservableHelpers
            .CreateMinutePulse(15)      // check every 15 seconds if it's a new minute
            .Subscribe(i => DoSomething());
    }

    public void Stop()
    {
        if(Timer != null)
        {
            Timer.Dispose();
            Timer = null;
        }
    }

    public void DoSomething()
    {
        // do your thing here
    }
}

public static class ObservableHelpers
{
    /// <summary>
    ///     Returns an observable that pulses every minute with the specified resolution.
    ///     The pulse occurs within the amount of time specified by the resolution (in seconds.)
    ///     Higher resolution (i.e. lower specified number of seconds) may affect execution speed.
    /// </summary>
    /// <returns></returns>
    public static IObservable<int> CreateMinutePulse(int resolution)
    {
        return Observable
            .Interval(TimeSpan.FromSeconds(resolution.SetWithinRange(1, 59)))
            .Select(i => DateTime.Now.Minute)
            .DistinctUntilChanged();
    }
}

Respondido el 10 de enero de 11 a las 10:01

@Mennan You need the Reactive Extensions dll msdn.microsoft.com/en-us/data/gg577609.aspx - Ryan

Well, I belive your problem seems to be solved with Producer Consumer Design pattern.

Producer will be the single main thread, and the all other threads will be the consumer thread. From my opinion it will be best have independent threads than using thread pool.

P.ej:

private Thread Worker;
public Consumer()
{
    Worker = new Thread(ProcessMethod);
}

Now in processmethod you do what you have to do. Create as many Consumer as you want.

Respondido el 10 de enero de 11 a las 10:01

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.