实现永无止境任务的正确方法。 (计时器与任务)

时间:2012-12-04 03:12:44

标签: c# multithreading timer task-parallel-library .net-4.5

因此,只要应用程序正在运行或请求取消,我的应用程序几乎需要连续执行操作(每次运行之间暂停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()取消它?

3 个答案:

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