正确的方式来实现一个永无止境的任务。 (定时器VS任务)(Proper way to imple

2019-06-21 12:20发布

所以,我的应用程序需要长达几乎连续执行动作(10秒左右,每次运行之间的停顿)作为应用程序正在运行或请求取消。 它需要做的工作有责任采取长达30秒的可能性。

是更好地使用System.Timers.Timer,并使用自动复位,确保之前的“滴答”已完成之前,不执行动作。

或者我应该使用LongRunning模式一般任务与取消标记,并有定期无限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.
}

或者只是使用一个简单的定时器,而使用其自动复位性能,并调用.Stop()来取消它?

Answer 1:

我会使用TPL数据流为这个(因为你正在使用.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数据流? 有几个原因:

关注点分离

CreateNeverEndingTask方法现在是一个工厂,创建你的“服务”可以这么说。 您可以控制,当它启动和停止,它是完全独立的。 你不必交织你的代码的其他方面定时器的状态控制。 您只需创建块,启动和停止它,当你完成。

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

在TPL数据流中的块的缺省调度程序为相同的Task ,这是线程池。 通过使用ActionBlock<TInput>处理您的行动,以及对呼叫Task.Delay ,你得到的时候,你实际上并不做任何事情,你正在使用的线程控制。 当然,这实际上导致一些开销,当你酿出了新的Task将处理的延续,但应该是小,考虑你是不是在紧密循环(你等待调用之间的10秒)处理此。

如果DoWork函数实际上可制成awaitable(即,在它返回一个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);
}


Answer 2:

我找到了新的基于任务的界面是做这样的事情很简单 - 比使用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()接受取消标记,并取消将更加敏感。



Answer 3:

以下是我想出了:

  • 继承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();
    }
}


文章来源: Proper way to implement a never ending task. (Timers vs Task)