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

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

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

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

或者我应该在 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() 来取消它?

最佳答案

我会使用 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称呼;如果过程被取消,取消将在第一个可能的机会发生。

从那里开始,很容易重构您的代码来存储 ITargetBlock<DateTimeoffset> interfaceActionBlock<TInput> 实现(这是表示作为消费者的 block 的更高级别的抽象,您希望能够通过调用 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方法现在可以说是一个创建“服务”的工厂。您可以控制它何时开始和停止,而且它是完全独立的。您不必将计时器的状态控制与代码的其他方面交织在一起。您只需创建 block ,启动它,并在完成后停止它。

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

TPL 数据流中 block 的默认调度程序对于 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);
}

关于c# - 实现永无止境的任务的正确方法。 (计时器与任务),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/13695499/

相关文章:

c# - child 不跟随 parent

C#/WPF : Disable Text-Wrap of RichTextBox

c# - 重新启动协程忽略 yield WaitForSeconds

c# - 如何将波斯日期转换为公历日期?

python - 结束循环正在终止线程

c# - 如何实现每小时运行一次但也可以从 .aspx 页面触发的作业?

java - 如何使用Java中的定时器来运行Job指定的时间?

java - 更改 Java 计时器中的延迟

Java:线程取决于文件

c++ - VS 7.1 Release 编译和多线程