执行永无止境的任务的正确方法.(计时器与任务) [英] Proper way to implement a never ending task. (Timers vs Task)

查看:31
本文介绍了执行永无止境的任务的正确方法.(计时器与任务)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

So, my app needs to perform an action almost continuously (with a pause of 10 seconds or so between each run) for as long as the app is running or a cancellation is requested. The work it needs to do has the possibility of taking up to 30 seconds.

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

Is it better to use a System.Timers.Timer and use AutoReset to make sure it doesn't perform the action before the previous "tick" has completed.

或者我应该在 LongRunning 模式下使用带有取消标记的常规任务,并在其中使用常规的无限 while 循环来调用操作,在两次调用之间使用 10 秒的 Thread.Sleep 完成工作?至于 async/await 模型,我不确定它在这里是否合适,因为我没有任何工作返回值.

Or should I use a general Task in LongRunning mode with a cancellation token, and have a regular infinite while loop inside it calling the action doing the work with a 10 second Thread.Sleep between calls? As for the async/await model, I'm not sure it would be appropriate here as I don't have any return values from the work.

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

or just use a simple timer while using its AutoReset property, and call .Stop() to cancel it?

推荐答案

我会使用 TPL Dataflow 为此(因为您使用的是 .NET 4.5 并且它使用 Task 内部).您可以轻松创建一个 ActionBlock 它在处理它的操作并等待适当的时间后将项目发布给自己.

I'd use TPL Dataflow for this (since you're using .NET 4.5 and it uses Task internally). You can easily create an ActionBlock<TInput> which posts items to itself after it's processed it's action and waited an appropriate amount of time.

首先,创建一个工厂来创建你永无止境的任务:

First, create a factory that will create your never-ending task:

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 来获取 DateTimeOffset 结构;你必须传递一个类型参数,它也可能传递一些有用的状态(如果你愿意,你可以改变状态的性质).

I've chosen the ActionBlock<TInput> to take a DateTimeOffset structure; you have to pass a type parameter, and it might as well pass some useful state (you can change the nature of the state, if you want).

另外,请注意 ActionBlock 默认情况下一次只处理 一个 项,因此您可以保证只处理一个操作(意味着,当它调用 重入 时,您将不必处理它="http://msdn.microsoft.com/en-us/library/hh194836.aspx" rel="noreferrer">Post 扩展方法 本身).

Also, note that the ActionBlock<TInput> by default processes only one item at a time, so you're guaranteed that only one action will be processed (meaning, you won't have to deal with reentrancy when it calls the Post extension method back on itself).

我还通过了 CancellationTokenActionBlock 的构造函数和 Task.Delay 方法 调用;如果流程被取消,将在第一时间取消.

I've also passed the CancellationToken structure to both the constructor of the ActionBlock<TInput> and to the Task.Delay method call; if the process is cancelled, the cancellation will take place at the first possible opportunity.

从那里开始,可以轻松重构代码以存储 ITargetBlock interfaceActionBlock 实现(这是表示作为消费者的块的更高级别的抽象,并且您希望能够触发消费通过调用 Post 扩展方法):

From there, it's an easy refactoring of your code to store the ITargetBlock<DateTimeoffset> interface implemented by ActionBlock<TInput> (this is the higher-level abstraction representing blocks that are consumers, and you want to be able to trigger the consumption through a call to the Post extension method):

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?几个原因:

Why would you want to use TPL Dataflow here? A few reasons:

关注点分离

CreateNeverEndingTask 方法现在是一个可以创建服务"的工厂.您可以控制它何时开始和停止,而且它是完全独立的.您不必将计时器的状态控制与代码的其他方面交织在一起.您只需创建块,启动它,并在完成后停止它.

The CreateNeverEndingTask method is now a factory that creates your "service" so to speak. You control when it starts and stops, and it's completely self-contained. You don't have to interweave state control of the timer with other aspects of your code. You simply create the block, start it, and stop it when you're done.

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

TPL 数据流中块的默认调度器与 Task 相同,它是线程池.通过使用 ActionBlock<TInput> 来处理您的操作,以及调用 Task.Delay,您可以控制您正在使用的线程'实际上并没有做任何事情.当然,当您生成将处理延续的新 Task 时,这实际上会导致一些开销,但这应该很小,考虑到您不是在紧密循环中处理它(您正在等待调用间隔十秒).

The default scheduler for the blocks in TPL data flow is the same for a Task, which is the thread pool. By using the ActionBlock<TInput> to process your action, as well as a call to Task.Delay, you're yielding control of the thread that you were using when you're not actually doing anything. Granted, this actually leads to some overhead when you spawn up the new Task that will process the continuation, but that should be small, considering you aren't processing this in a tight loop (you're waiting ten seconds between invocations).

如果 DoWork 函数实际上可以成为可等待的(即,因为它返回一个 Task),那么您可以(可能)通过调整上面的工厂方法取一个Func 而不是 Action,像这样:

If the DoWork function actually can be made awaitable (namely, in that it returns a Task), then you can (possibly) optimize this even more by tweaking the factory method above to take a Func<DateTimeOffset, CancellationToken, Task> instead of an Action<DateTimeOffset>, like so:

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 编织到您的方法中(如果它接受一个),这在此处完成.

Of course, it would be good practice to weave the CancellationToken through to your method (if it accepts one), which is done here.

这意味着您将拥有一个带有以下签名的 DoWorkAsync 方法:

That means you would then have a DoWorkAsync method with the following signature:

Task DoWorkAsync(CancellationToken cancellationToken);

您必须更改(只是稍微更改,并且不会在此处泄露关注点分离)StartWork 方法以说明传递给 CreateNeverEndingTask方法,像这样:

You'd have to change (only slightly, and you're not bleeding out separation of concerns here) the StartWork method to account for the new signature passed to the CreateNeverEndingTask method, like so:

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);
}

这篇关于执行永无止境的任务的正确方法.(计时器与任务)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆