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

C# 执行永无止境任务的正确方法。(计时器与任务),c#,multithreading,timer,task-parallel-library,.net-4.5,C#,Multithreading,Timer,Task Parallel Library,.net 4.5,因此,只要应用程序正在运行或请求取消,我的应用程序几乎需要连续执行一项操作(每次运行之间暂停10秒左右)。它需要做的工作可能需要30秒 是否最好使用System.Timers.Timer并使用AutoReset确保在上一次“勾选”完成之前不执行操作 或者我应该在长时间运行模式下使用一个带有取消令牌的常规任务,并在其中有一个常规的无限while循环,用10秒的线程调用操作。在调用之间休眠?至于async/await模型,我不确定它在这里是否合适,因为我没有任何工作返回值 CancellationT

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

是否最好使用System.Timers.Timer并使用AutoReset确保在上一次“勾选”完成之前不执行操作

或者我应该在长时间运行模式下使用一个带有取消令牌的常规任务,并在其中有一个常规的无限while循环,用10秒的线程调用操作。在调用之间休眠?至于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()来取消它?

我发现新的基于任务的界面对于这样的操作非常简单,甚至比使用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=task.Run(async()=>/我会使用它(因为您使用的是.NET 4.5,它在内部使用)。您可以轻松创建一个在处理它的操作并等待适当时间后将项目发布到它自己的程序

首先,创建一个工厂,该工厂将创建您永无止境的任务:

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;
}
您的
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);
}
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;
}
然后是您的
停止工作
方法:

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);
}
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数据流中块的默认调度程序与
任务
的默认调度程序相同,后者是线程池。通过使用
操作块
来处理您的操作,以及调用
任务.Delay
,您可以控制您在实际不做任何事情时使用的线程。当然,这是实际的当您生成新的
任务来处理延续时,ly会导致一些开销,但考虑到您没有在一个紧密的循环中处理它(在调用之间等待10秒),这应该很小

如果
DoWork
函数实际上是可等待的(即,它返回一个
任务
),那么您可以(可能)通过调整上面的工厂方法来采取一个而不是
操作
,从而对此进行进一步优化,如下所示:

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;
}
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);
}
您必须更改
StartWork
方法(只需稍加更改,并且这里没有漏掉关注点的分离),以说明传递给
createNeverdingTask
方法的新签名,如下所示:

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

以下是我的想法:

  • NeverEndingTask
    继承,并用您想要做的工作覆盖
    ExecutionCore
    方法
  • 更改
    ExecutionLoopDelayMs
    允许您调整循环之间的时间,例如,如果您想使用退避算法
  • 启动/停止
    提供启动/停止任务的同步接口
  • LongRunning
    意味着每个
    NeverEndingTask
    将获得一个专用线程
  • 与上面基于
    ActionBlock
    的解决方案不同,此类不会在循环中分配内存
  • 下面的代码是草图,不一定是生产代码:)
:


考虑到你想要达到的目标,这项任务似乎是过火了。在OnTick()开始时停止计时器,检查bool以查看是否应该在上执行任何操作,执行工作,完成后重新启动计时器。您好,我正在尝试此实现,但我面临问题。如果我的DoWork不带参数,task=CreateNeverEndingTask(现在=>DoWork(),wtoken.Token);给我一个生成错误(类型不匹配)。另一方面,如果我的DoWork接受DateTimeOffset参数,则同一行会给我一个不同的生成错误,告诉我DoWork的重载不接受0个参数。你能帮我解决这个问题吗?事实上,我在分配任务的行中添加了一个cast,并将参数传递给DoWork:task=(ActionBlock)CreateNeverEndingTask(now=>DoWork(now),wtoken.Token);您还可以将“ActionBlock任务”的类型更改为ITargetBlock任务;我相信这可能会永远分配内存,从而最终导致溢出。@NateGardner在哪一部分?如果使用异步lambda作为task.Factory.StartNew的参数,您将得到什么任务?当您执行task.Wait()时;请求取消后,您将等待不正确的任务。是的,这实际上应该是任务。立即运行,它具有正确的重载。根据它看起来像
task。Run
使用线程池,因此,您的示例使用
Task.Run
而不是
Task.Factory.StartNew
TaskCreationOptions.LongRunning
做的事情并不完全相同-如果我需要任务使用
LongRunning
选项,我是否会无法使用
Task.Run
,如您所示,还是我遗漏了什么?@Lumirris:async/await的要点是避免在线程执行的整个过程中占用线程(这里,在延迟调用期间,任务没有使用线程)。因此,使用
longlunning
与不占用线程的目标是不兼容的。如果您想保证在它自己的线程上运行,您可以使用它,但是在这里您将启动一个大部分时间处于休眠状态的线程。用例是什么?@Lu