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


92

因此,只要应用程序正在运行或要求取消,我的应用程序就需要几乎连续地执行操作(每次运行之间有10秒左右的暂停)。它需要做的工作可能要花费30秒。

最好使用System.Timers.Timer并使用AutoReset确保在上一个“刻度”完成之前它不执行操作。

还是我应该在LongRunning模式下使用带有取消令牌的常规Task,并在其中执行常规的无限while循环,以调用之间用10秒的Thread.Sleep进行操作来执行操作?至于异步/等待模型,我不确定这里是否合适,因为我没有任何返回值。

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()取消它?


考虑到您要实现的目标,任务似乎有些过分。en.wikipedia.org/wiki/KISS_principle。在OnTick()开始时停止计时器,检查布尔值以查看是否应该对计时器进行任何操作,进行工作,并在完成后重新启动Timer。
Mike Trusov

Answers:


94

我会用 为此 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结构 ; 您必须传递一个类型参数,它也可能传递一些有用的状态(您可以根据需要更改状态的性质)。

另外,请注意,ActionBlock<TInput>默认情况下,一次只能处理一项,因此可以确保仅处理一项操作(这意味着您不必处理当它再次调用Post扩展方法重入)。

我还将该CancellationToken结构传递给了ActionBlock<TInput>Task.Delay方法调用;如果取消了该过程,则取消将在第一个可能的机会发生。

从那里开始,很容易对代码进行重构,以存储由实现的ITargetBlock<DateTimeoffset>接口ActionBlock<TInput>(这是代表作为使用者的块的高级抽象,您希望能够通过调用来触发使用。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现在,方法是一家工厂,可以创建您的“服务”。您可以控制它的启动和停止时间,它是完全独立的。您不必将计时器的状态控制与代码的其他方面交织在一起。您只需创建一个块,然后启动它,然后在完成时停止它。

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

对于Task线程池,TPL数据流中块的默认调度程序与相同。通过使用ActionBlock<TInput>来处理您的操作以及对的调用Task.Delay,您可以在实际上不执行任何操作时控制所使用的线程。当然,当您生成Task将处理延续的新内容时,这实际上会导致一些开销,但是考虑到您不是在紧密的循环中进行处理(在两次调用之间等待十秒钟),这应该很小。

如果DoWork实际上可以使该函数处于等待状态(即,它返回Task),那么您可以(可能)通过调整上面的factory方法来采用a 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);
}

您好,我正在尝试此实现,但遇到了问题。如果我的DoWork不带任何参数,则task = CreateNeverEndingTask(now => DoWork(),wtoken.Token); 给我一个构建错误(类型不匹配)。另一方面,如果我的DoWork使用DateTimeOffset参数,则同一行会给我一个不同的生成错误,告诉我DoWork的任何重载都不会使用0参数。您能帮我解决这个问题吗?
Bovaz 2014年

1
实际上,我通过在分配任务的行中添加强制转换并将参数传递给DoWork来解决我的问题:task =(ActionBlock <DateTimeOffset>)CreateNeverEndingTask(now => DoWork(now),wtoken.Token);
Bovaz 2014年

您也可以更改“ ActionBlock <DateTimeOffset>任务”的类型;到ITargetBlock <DateTimeOffset>任务;
XOR

1
我相信这很可能会永远分配内存,从而最终导致溢出。
内特·加德纳

@NateGardner在哪一部分?
casperOne

75

我发现新的基于任务的界面对于执行此类操作非常简单-比使用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.Delayover Thread.Sleep意味着您不会在睡眠期间束缚什么也不做。

如果有能力的话,您还可以DoWork()接受取消令牌,这样取消响应就会更快。


1
艳记,如果你使用异步lambda作为Task.Factory.StartNew的参数,你会得到什么样的任务- blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx当你做task.Wait( ); 请求取消后,您将等待不正确的任务。
卢卡斯·皮尔克

是的,这实际上应该是Task.Run现在,它具有正确的重载。
porges

根据http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx,它看起来像是在Task.Run使用线程池,因此您的示例使用Task.Run代替Task.Factory.StartNewwith TaskCreationOptions.LongRunning并不会完全相同-如果我需要使用该LongRunning选项的任务,我将无法Task.Run像您显示的那样使用它,或者我错过了什么?
杰夫2015年

@Lumirris:async / await的目的是避免在整个执行过程中占用线程(这里,在Delay调用期间,任务未使用线程)。因此,使用LongRunning与不占用线程这一目标是不兼容的。如果要保证在其自己的线程上运行,可以使用它,但是在这里,您将启动一个大部分时间都在休眠的线程。用例是什么?
porges

@Porges点。我的用例是一个运行无限循环的任务,其中每个迭代将执行大量工作,然后在第二个迭代中进行另一次工作之前“放松” 2秒钟。它永远运行,但要定期休息2秒。不过,我的评论更多是关于您是否可以LongRunning使用Task.Run语法来指定它。从文档Task.Run中看,只要您对它使用的默认设置感到满意,它看起来就是更简洁的语法。似乎没有一个带有TaskCreationOptions参数的重载。
杰夫

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();
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.