【发布时间】:2012-06-25 13:51:29
【问题描述】:
我想创建一个 Windows 服务,它会创建 x 个线程,每 x 分钟唤醒一次并做一些工作。
我认为任务调度或并行框架不适合此类工作,因为它最适合开始、完成和结束的工作,而不是恒定的工作。
我应该考虑为这种方法使用线程池还是有人对一个好的解决方案有任何建议?
【问题讨论】:
我想创建一个 Windows 服务,它会创建 x 个线程,每 x 分钟唤醒一次并做一些工作。
我认为任务调度或并行框架不适合此类工作,因为它最适合开始、完成和结束的工作,而不是恒定的工作。
我应该考虑为这种方法使用线程池还是有人对一个好的解决方案有任何建议?
【问题讨论】:
真的,听起来你只需要一个线程。
这是我为这类事情创建的辅助类。使用方法如下:
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();
(或者,使用 backgroundThread: true 调用 Start,然后您不需要调用 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)。
希望有用!
【讨论】:
Periodic 的 Threading.Timer 方法有何不同)。Observable.Interval(Timespan.FromMinutes(15)).Subscribe(i => CheckForTasksDueAndStartThem());。我在许多企业服务中使用这种代码(从每天 1 个动作到每秒几十个动作)。
当您故意不让它们在 y 分钟内完成任何工作时,启动 x 个线程来执行 x 个作业是没有意义的。只需要 一个 线程做 x 个工作。完成这项工作需要 x 倍的时间(实际上要少一点),但只要不到 y 分钟,这根本不是问题。
这样做的额外好处是该服务不会轻易影响机器的响应能力,其他内核仍然可用。并且您的代码变得更容易实现和调试。
使用 System.Threading.Thread 计时器来激活工作。回调在线程池线程上运行。启动和停止服务很容易,只需启用/禁用该计时器即可。
【讨论】:
您真的需要这些线程持续运行并在 x 分钟后唤醒吗?我认为您可能需要考虑使用现有的调度程序库,例如 Quartz.NET,它会为您处理运行任务。
【讨论】:
我有两个建议给你。首先,要构建您的服务,请查看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();
}
}
【讨论】:
好吧,我相信您的问题似乎可以通过生产者消费者设计模式解决。
生产者将是单个主线程,所有其他线程将是消费者线程。 在我看来,最好有独立的线程而不是使用线程池。
例如:
private Thread Worker;
public Consumer()
{
Worker = new Thread(ProcessMethod);
}
现在在 processmethod 中,您可以做您必须做的事情。 创建任意数量的消费者。
【讨论】: