C# 执行永无止境的任务的正确方法。(计时器与任务)

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/13695499/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-10 09:27:21  来源:igfitidea点击:

Proper way to implement a never ending task. (Timers vs Task)

c#multithreadingtimertask-parallel-library.net-4.5

提问by Josh

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.

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

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.

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

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.

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

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

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

采纳答案by casperOne

I'd use TPL Dataflowfor this (since you're using .NET 4.5 and it uses Taskinternally). 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.

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

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

I've chosen the ActionBlock<TInput>to take a DateTimeOffsetstructure; 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<TInput>采用DateTimeOffset结构;您必须传递一个类型参数,它也可能传递一些有用的状态(如果需要,您可以更改状态的性质)。

Also, note that the ActionBlock<TInput>by default processes only oneitem at a time, so you're guaranteed that only one action will be processed (meaning, you won't have to deal with reentrancywhen it calls the Postextension methodback on itself).

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

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

我还将CancellationToken结构传递给 的构造函数ActionBlock<TInput>Task.Delay方法调用;如果流程被取消,将在第一时间取消。

From there, it's an easy refactoring of your code to store the ITargetBlock<DateTimeoffset>interfaceimplemented 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 Postextension method):

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

CancellationTokenSource wtoken;
ActionBlock<DateTimeOffset> task;

Your StartWorkmethod:

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

And then your StopWorkmethod:

然后你的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;
}

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

为什么要在这里使用 TPL Dataflow?几个原因:

Separation of concerns

关注点分离

The CreateNeverEndingTaskmethod 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.

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

More efficient use of threads/tasks/resources

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

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 Taskthat 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).

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

If the DoWorkfunction 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:

如果该DoWork函数实际上可以成为可等待的(即,因为它返回 a Task),那么您可以(可能)通过调整上面的工厂方法以采用 aFunc<DateTimeOffset, CancellationToken, Task>而不是 an来进一步优化它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;
}

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

当然,最好将CancellationToken整个方法编织到您的方法中(如果它接受一个),这在此处完成。

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

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

Task DoWorkAsync(CancellationToken cancellationToken);

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

您必须更改(只是稍微更改,并且您不会在这里泄漏关注点的分离)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);
}

回答by porges

I find the new Task-based interface to be very simple for doing things like this - even easier than using the Timer class.

我发现新的基于任务的界面对于做这样的事情非常简单 - 甚至比使用 Timer 类更容易。

There are some small adjustments you can make to your example. Instead of:

您可以对示例进行一些小的调整。代替:

task = Task.Factory.StartNew(() =>
{
    while (true)
    {
        wtoken.Token.ThrowIfCancellationRequested();
        DoWork();
        Thread.Sleep(10000);
    }
}, wtoken, TaskCreationOptions.LongRunning);

You can do this:

你可以这样做:

task = Task.Run(async () =>  // <- marked async
{
    while (true)
    {
        DoWork();
        await Task.Delay(10000, wtoken.Token); // <- await with cancellation
    }
}, wtoken.Token);

This way the cancellation will happen instantaneously if inside the Task.Delay, rather than having to wait for the Thread.Sleepto finish.

这样,如果在 内部Task.Delay,取消将立即发生,而不必等待Thread.Sleep完成。

Also, using Task.Delayover Thread.Sleepmeans you aren't tying up a thread doing nothing for the duration of the sleep.

此外,使用Task.DelayoverThread.Sleep意味着您不会在睡眠期间无所事事地占用线程。

If you're able, you can also make DoWork()accept a cancellation token, and the cancellation will be much more responsive.

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

回答by Hyman Ukleja

Here is what I came up with:

这是我想出的:

  • Inherit from NeverEndingTaskand override the ExecutionCoremethod with the work you want to do.
  • Changing ExecutionLoopDelayMsallows you to adjust the time between loops e.g. if you wanted to use a backoff algorithm.
  • Start/Stopprovide a synchronous interface to start/stop task.
  • LongRunningmeans you will get one dedicated thread per NeverEndingTask.
  • This class does not allocate memory in a loop unlike the ActionBlockbased solution above.
  • The code below is sketch, not necessarily production code :)
  • 使用您想要执行的工作继承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();
    }
}