97

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

使用 System.Timers.Timer 并使用 AutoReset 确保它在前一个“滴答”完成之前不执行操作是否更好。

或者我应该在 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.
}

或者只是在使用其 AutoReset 属性时使用一个简单的计时器,然后调用 .Stop() 来取消它?

4

3 回答 3

97

我会为此使用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 数据流中块的默认调度程序与 a 相同Task,即线程池。通过使用ActionBlock<TInput>来处理您的操作以及对 的调用Task.Delay,您可以在您实际上没有做任何事情时让出对您正在使用的线程的控制。诚然,当您生成将处理延续的新时,这实际上会导致一些开销Task,但这应该很小,考虑到您没有在一个紧密的循环中处理这个(您在调用之间等待十秒钟)。

如果该DoWork函数实际上可以被设置为可等待的(即,它返回 a Task),那么您可以(可能)通过调整上面的工厂方法以采用 aFunc<DateTimeOffset, CancellationToken, Task>而不是a 来进一步优化它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);
}
于 2012-12-04T21:54:58.190 回答
79

我发现新的基于任务的界面对于执行此类操作非常简单——甚至比使用 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.DelayoverThread.Sleep意味着您不会在睡眠期间占用任何不做任何事情的线程。

如果可以的话,您还可以DoWork()接受取消令牌,并且取消的响应速度会更快。

于 2012-12-04T03:33:55.580 回答
6

这是我想出的:

  • 继承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();
    }
}
于 2016-06-07T00:29:15.800 回答