因此,只要应用程序正在运行或请求取消,我的应用程序几乎需要连续执行操作(每次运行之间暂停10秒左右)。它需要做的工作可能需要30秒。
最好使用System.Timers.Timer并使用AutoReset确保它在前一个“tick”完成之前不执行操作。
或者我应该在LongRunning模式下使用带有取消令牌的常规任务,并且在其内部有一个常规的无限while循环调用在调用之间执行10秒Thread.Sleep的操作?至于async / await模型,我不确定它在这里是否合适,因为我没有任何工作的返回值。
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()取消它?
答案 0 :(得分:91)
我会使用TPL Dataflow(因为你使用的是.NET 4.5,它在内部使用Task
)。您可以轻松创建一个ActionBlock<TInput>
,在项目处理完动作后会将项目发布给自己,并等待适当的时间。
首先,创建一个工厂,创建永无止境的任务:
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;
}
我选择了ActionBlock<TInput>
来DateTimeOffset
structure;你必须传递一个类型参数,它也可以传递一些有用的状态(如果你愿意,你可以改变状态的性质)。
另请注意,ActionBlock<TInput>
默认情况下一次仅处理一个项目,因此您可以保证只处理一个操作(意味着您不会处理reentrancy时自己回复Post
extension method。
我还将CancellationToken
structure传递给ActionBlock<TInput>
和Task.Delay
method电话的构造函数;如果该过程被取消,取消将在第一时间进行。
从那里开始,您可以轻松地重构代码以存储ActionBlock<TInput>
实现的ITargetBlock<DateTimeoffset>
interface(这是代表消费者的块的更高级抽象,并且您希望能够触发通过调用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
方法现在是一个可以创建“服务”的工厂。你控制它何时开始和停止,它完全是独立的。您不必将计时器的状态控制与代码的其他方面交织在一起。您只需创建块,启动它,并在完成后停止它。
更有效地使用线程/任务/资源
TPL数据流中块的默认调度程序与Task
(即线程池)相同。通过使用ActionBlock<TInput>
来处理您的操作,以及对Task.Delay
的调用,您可以控制在您实际上没有执行任何操作时使用的线程。当然,当你产生将处理延续的新Task
时,这实际上会导致一些开销,但是这应该很小,考虑到你没有在紧密循环中处理这个(你在等待十秒之间)调用)。
如果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);
}
答案 1 :(得分:69)
我发现新的基于任务的界面对于这样的事情来说非常简单 - 比使用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
完成。
此外,使用Task.Delay
而不是Thread.Sleep
意味着你不会在一段时间内一直没有做任何事情。
如果您有能力,您还可以DoWork()
接受取消令牌,取消将更加快速响应。
答案 2 :(得分:4)
以下是我提出的建议:
NeverEndingTask
并使用您要执行的工作覆盖ExecutionCore
方法。ExecutionLoopDelayMs
可让您调整循环之间的时间,例如如果你想使用退避算法。Start/Stop
提供启动/停止任务的同步界面。LongRunning
表示每个NeverEndingTask
将获得一个专用线程。ActionBlock
的解决方案不同,此类不会在循环中分配内存。
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();
}
}