正确的方法来实现一个永无止境的任务。 (定时器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()
接受一个取消令牌,取消将会更加快速。
这是我想出来的:
- 从
NeverEndingTask
inheritance,并使用您要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(); } }