Лучшее решение для службы Windows с постоянно работающими потоками в C # 4.0 - PullRequest
3 голосов
/ 09 января 2011

Я хочу создать службу Windows, которая будет создавать x количество потоков, которые просыпаются каждые x минут и выполняют некоторую работу.

Я думаю, что планирование задач или параллельная структура плохо подходят для этоготип работы, который лучше всего подходит для работы, которая начинается, завершается и заканчивается, а не является постоянной.

Стоит ли рассматривать использование пула потоков для этого подхода, или у кого-нибудь есть какие-либо советы для хорошего решения?1005 *

Ответы [ 5 ]

4 голосов
/ 09 января 2011

Действительно, звучит так, будто вам нужен только один поток.

Вот вспомогательный класс, который я создал именно для такого рода вещей. Вот как вы это используете:

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 */ }
}

Затем для запуска задач:

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

А при отключении сервиса:

tasks.Shutdown();

(альтернативно, вызовите Start с backgroundThread: true, тогда вам не нужно вызывать Shutdown, но тогда задача может быть просто завершена прямо в середине выполнения чего-либо)

Вот фактический код:

/// <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();
        }
    }
}

Поток проводит большую часть времени в спящем режиме, но он просыпается каждую 1 секунду, чтобы проверить, не выполнено ли задание. Этот 1-секундный интервал, вероятно, слишком короткий для интервалов, например, 15 минут, поэтому вместо этого уменьшите его до 30 секунд (это будет SubsequentInterval).

Надеюсь, это полезно!

3 голосов
/ 09 января 2011

Нет особого смысла запускать x-потоки для выполнения x-заданий, когда вы намеренно не позволяете им выполнять какую-либо работу в течение y минут.Просто имейте один поток, делающий x заданий.Выполнение работы займет х раз дольше (на самом деле немного меньше), но это совсем не проблема, если это займет менее y минут.

Дополнительные преимущества в том, что сервис не может легко повлиятьотзывчивость машины, остальные ядра остаются доступными.И ваш код становится чертовски легким для реализации и отладки.

Используйте таймер System.Threading.Thread, чтобы активировать работу.Обратный вызов выполняется в потоке потоков.Запуск и остановка службы просты, просто включите / отключите этот таймер.

1 голос
/ 10 января 2011

У меня есть два предложения для вас. Во-первых, для построения вашего сервиса, проверьте TopShelf . Это устраняет все трудности настройки службы Windows.

Во-вторых, вы можете использовать класс Observable для создания таймера, не прибегая к написанию кода, специфичного для таймера, или Quartz (боль в настройке!).

Вот пример кода:

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();
    }
}
1 голос
/ 09 января 2011

Вы действительно нуждаетесь в том, чтобы эти потоки работали постоянно, а затем просыпались через x минут? Я думаю, что вы можете рассмотреть возможность использования существующей библиотеки планировщика, такой как Quartz.NET , которая обрабатывает выполнение задач для вас.

1 голос
/ 09 января 2011

Что ж, я полагаю, что ваша проблема, похоже, решена с помощью шаблона Producer Consumer Design.

Producer будет единственным основным потоком, а все остальные потоки будут потребительским потоком.По моему мнению, лучше иметь независимые потоки, чем использовать пул потоков.

Например:

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

Теперь в processmethod вы делаете то, что должны делать.Создайте столько потребителей, сколько вы хотите.

...