正确的方法来实现一个永无止境的任务。 (定时器vs任务)

所以,只要应用程序正在运行或请求取消,我的应用程序就需要几乎连续地执行一个操作(每次运行之间暂停10秒左右)。 它需要做的工作可能需要长达30秒。

使用System.Timers.Timer并使用AutoReset确保它在上一个“打勾”完成之前不会执行操作会更好吗?

还是应该使用带有取消标记的LongRunning模式的常规任务,并在其中调用执行工作的动作,并在调用之间使用10秒的Thread.Sleep? 至于asynchronous/等待模式,我不确定这是否合适,因为我没有任何工作返回值。

CancellationTokenSource wtoken; Task task; void StopWork() { wtoken.Cancel(); try { task.Wait(); } catch(AggregateException) { } } void StartWork() { wtoken = new CancellationTokenSource(); task = Task.Factory.StartNew(() => { while (true) { wtoken.Token.ThrowIfCancellationRequested(); DoWork(); Thread.Sleep(10000); } }, wtoken, TaskCreationOptions.LongRunning); } void DoWork() { // Some work that takes up to 30 seconds but isn't returning anything. } 

或者只使用一个简单的计时器,而使用它的AutoReset属性,并调用.Stop()取消它?

我会使用TPL数据stream (因为你使用.NET 4.5,它内部使用Task )。 你可以很容易地创build一个ActionBlock<TInput> ,它在处理它的动作并等待适当的时间之后把项目发送给自己。

首先,创build一个工厂,创build你永无止境的任务:

 ITargetBlock<DateTimeOffset> CreateNeverEndingTask( Action<DateTimeOffset> action, CancellationToken cancellationToken) { // Validate parameters. if (action == null) throw new ArgumentNullException("action"); // Declare the block variable, it needs to be captured. ActionBlock<DateTimeOffset> block = null; // Create the block, it will call itself, so // you need to separate the declaration and // the assignment. // Async so you can wait easily when the // delay comes. block = new ActionBlock<DateTimeOffset>(async now => { // Perform the action. action(now); // Wait. await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken). // Doing this here because synchronization context more than // likely *doesn't* need to be captured for the continuation // here. As a matter of fact, that would be downright // dangerous. ConfigureAwait(false); // Post the action back to the block. block.Post(DateTimeOffset.Now); }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken }); // Return the block. return block; } 

我select了ActionBlock<TInput>采取DateTimeOffset结构 ; 你必须传递一个types参数,它可能会传递一些有用的状态(如果你愿意,你可以改变状态的性质)。

另外,请注意ActionBlock<TInput>在默认情况下一次只处理一个项目,所以你可以保证只处理一个动作(也就是说,当你调用Post扩展方法时你不必处理重入回到本身)。

我还将CancellationToken结构传递给ActionBlock<TInput>的构造函数和Task.Delay方法调用; 如果该过程被取消,取消将在第一个可能的机会进行。

从那里开始,对代码进行简单的重构来存储由ActionBlock<TInput>实现的ITargetBlock<DateTimeoffset>接口 (这是表示消费者块的更高级的抽象,并且您希望能够通过调用Post扩展方法):

 CancellationTokenSource wtoken; ActionBlock<DateTimeOffset> task; 

你的StartWork方法:

 void StartWork() { // Create the token source. wtoken = new CancellationTokenSource(); // Set the task. task = CreateNeverEndingTask(now => DoWork(), wtoken.Token); // Start the task. Post the time. task.Post(DateTimeOffset.Now); } 

然后你的StopWork方法:

 void StopWork() { // CancellationTokenSource implements IDisposable. using (wtoken) { // Cancel. This will cancel the task. wtoken.Cancel(); } // Set everything to null, since the references // are on the class level and keeping them around // is holding onto invalid state. wtoken = null; task = null; } 

你为什么要在这里使用TPL Dataflow? 几个原因:

关注点分离

CreateNeverEndingTask方法现在是一个创build“服务”的工厂。 您可以控制何时启动和停止,并且它是完全独立的。 您不必与代码的其他方面交织对定时器的状态控制。 您只需创build块,启动它,并在完成后停止。

更有效地使用线程/任务/资源

TPL数据stream中块的默认调度程序对于Task (即线程池)而言是相同的。 通过使用ActionBlock<TInput>来处理你的动作,以及对Task.Delay的调用,你可以控制当你没有做任何事情时使用的线程。 当然,这实际上会导致一些开销,当你产生新的Task ,将处理延续,但这应该是小的,考虑到你没有在一个紧密的循环处理(你等待10秒钟之间调用)。

如果DoWork函数实际上可以等待(即返回一个Task ),那么你可以(可能)通过调整上面的工厂方法来进行优化,使得Func<DateTimeOffset, CancellationToken, Task>而不是Action<DateTimeOffset> ,如下所示:

 ITargetBlock<DateTimeOffset> CreateNeverEndingTask( Func<DateTimeOffset, CancellationToken, Task> action, CancellationToken cancellationToken) { // Validate parameters. if (action == null) throw new ArgumentNullException("action"); // Declare the block variable, it needs to be captured. ActionBlock<DateTimeOffset> block = null; // Create the block, it will call itself, so // you need to separate the declaration and // the assignment. // Async so you can wait easily when the // delay comes. block = new ActionBlock<DateTimeOffset>(async now => { // Perform the action. Wait on the result. await action(now, cancellationToken). // Doing this here because synchronization context more than // likely *doesn't* need to be captured for the continuation // here. As a matter of fact, that would be downright // dangerous. ConfigureAwait(false); // Wait. await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken). // Same as above. ConfigureAwait(false); // Post the action back to the block. block.Post(DateTimeOffset.Now); }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken }); // Return the block. return block; } 

当然,将CancellationToken编织到您的方法(如果它接受一个方法)是一个很好的做法,这是在这里完成的。

这意味着您将有一个具有以下签名的DoWorkAsync方法:

 Task DoWorkAsync(CancellationToken cancellationToken); 

你必须改变(只有轻微的,而且你没有把这里的问题分离出来) StartWork方法来说明传递给CreateNeverEndingTask方法的新签名,如下所示:

 void StartWork() { // Create the token source. wtoken = new CancellationTokenSource(); // Set the task. task = CreateNeverEndingTask((now, ct) => DoWorkAsync(ct), wtoken.Token); // Start the task. Post the time. task.Post(DateTimeOffset.Now, wtoken.Token); } 

我发现新的基于任务的接口对于像这样的事情是非常简单的 – 甚至比使用Timer类更容易。

你可以对你的例子进行一些小的调整。 代替:

 task = Task.Factory.StartNew(() => { while (true) { wtoken.Token.ThrowIfCancellationRequested(); DoWork(); Thread.Sleep(10000); } }, wtoken, TaskCreationOptions.LongRunning); 

你可以这样做:

 task = Task.Run(async () => // <- marked async { while (true) { DoWork(); await Task.Delay(10000, wtoken.Token); // <- await with cancellation } }, wtoken.Token); 

这样,取消将在Task.Delay内部即时发生,而不必等待Thread.Sleep完成。

此外,使用Thread.Sleep Task.Delay意味着你不占用睡眠期间无所事事的线程。

如果可以的话,你也可以让DoWork()接受一个取消令牌,取消将会更加快速。

这是我想出来的:

  • NeverEndingTaskinheritance,并使用您要ExecutionCore的工作重写ExecutionCore方法。
  • 更改ExecutionLoopDelayMs允许您调整循环之间的时间,例如,如果您想使用退避algorithm。
  • Start/Stop提供一个同步界面来启动/停止任务。
  • LongRunning表示您将会为每个NeverEndingTask获得一个专用线程。
  • 与上面的基于ActionBlock的解决scheme不同,此类不会在循环中分配内存。
  • 下面的代码是草图,不一定是生产代码:)

 public abstract class NeverEndingTask { // Using a CTS allows NeverEndingTask to "cancel itself" private readonly CancellationTokenSource _cts = new CancellationTokenSource(); protected NeverEndingTask() { TheNeverEndingTask = new Task( () => { // Wait to see if we get cancelled... while (!_cts.Token.WaitHandle.WaitOne(ExecutionLoopDelayMs)) { // Otherwise execute our code... ExecutionCore(_cts.Token); } // If we were cancelled, use the idiomatic way to terminate task _cts.Token.ThrowIfCancellationRequested(); }, _cts.Token, TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning); // Do not forget to observe faulted tasks - for NeverEndingTask faults are probably never desirable TheNeverEndingTask.ContinueWith(x => { Trace.TraceError(x.Exception.InnerException.Message); // Log/Fire Events etc. }, TaskContinuationOptions.OnlyOnFaulted); } protected readonly int ExecutionLoopDelayMs = 0; protected Task TheNeverEndingTask; public void Start() { // Should throw if you try to start twice... TheNeverEndingTask.Start(); } protected abstract void ExecutionCore(CancellationToken cancellationToken); public void Stop() { // This code should be reentrant... _cts.Cancel(); TheNeverEndingTask.Wait(); } }