以指定的时间间隔定期运行异步方法

Posted

技术标签:

【中文标题】以指定的时间间隔定期运行异步方法【英文标题】:Run async method regularly with specified interval 【发布时间】:2015-08-08 08:03:31 【问题描述】:

我需要从 C# Web 应用程序向服务发布一些数据。数据本身是在用户使用应用程序时收集的(一种使用情况统计)。我不想在每个用户的请求期间向服务发送数据,我宁愿在应用程序中收集数据,然后在一个单独的线程中的单个请求中发送所有数据,这不服务于用户的请求(我的意思是用户不必等待服务处理请求)。为此,我需要一种 JS 的 setInterval 模拟 - 每 X 秒启动该函数以将所有收集到的数据刷新到服务。

我发现Timer 类提供了一些类似的(Elapsed 事件)。但是,这允许该方法只运行一次,但这不是一个大问题。它的主要困难是它需要签名

void MethodName(object e, ElapsedEventArgs args)

虽然我想启动异步方法,但它会调用网络服务(输入参数并不重要):

async Task MethodName(object e, ElapsedEventArgs args)

谁能建议如何解决所描述的任务?任何提示表示赞赏。

【问题讨论】:

如果Timer不想与您合作“AutoReset - 获取或设置一个值,该值指示Timer是否应该在每次指定的时间间隔过后或仅在第一次之后引发Elapsed事件过去了。”因为您拒绝阅读您提供的链接而不是运气不好...... :) 旁注 - 如果您需要可靠的间隔/避免流程循环,通常最好使用外部通知 - “bing.com/search?q=asp.net+repeating+task”应该给你一些信息(或“ASP.Net 后台任务”)。 @AlexeiLevenkov 不错的嘲讽。感谢您的提示,下次会更仔细地探索。但是,它并没有解决主要问题。 因为它没有解决主要问题,所以它是一个评论...... i3arnon 的回答为您提供了如何以火和忘记的方式调用异步的链接(也称为“从事件处理程序调用异步方法”) ... 【参考方案1】:

async 等效项是带有Task.Delaywhile 循环(内部使用System.Threading.Timer):

public async Task PeriodicFooAsync(TimeSpan interval, CancellationToken cancellationToken)

    while (true)
    
        await FooAsync();
        await Task.Delay(interval, cancellationToken)
    

传递CancellationToken 很重要,这样您就可以在需要时停止该操作(例如,当您关闭应用程序时)。

现在,虽然这通常与 .Net 相关,但在 ASP.Net 中,做任何形式的火灾和忘记都是危险的。有几种解决方案(如 HangFire),其中一些记录在 Fire and Forget on ASP.NET by Stephen Cleary 中,另一些记录在 How to run Background Tasks in ASP.NET by Scott Hanselman

【讨论】:

我应该写“await PeriodicFooAsync”还是只写“var result = PeriodicFooAsync”?在这种情况下我们需要等待吗? @Arvand 这取决于您是否要等待操作完成。 result 在您的情况下只会保留任务,之后的下一行将立即发生。 await 将异步“阻塞”直到周期性任务被取消。 @Arvand 您可能希望将该任务保存在某处而不等待它(假设它永远存在直到您的应用程序关闭)然后您可以在关闭之前等待它。 由于我有多个 PeriodicAsycMethods 我当然应该保存任务并避免使用等待,谢谢。 这不是周期性的,因为await FooAsync 的执行时间不为零。例如,如果FooAsync 需要 500 毫秒并且每小时运行一次,那么您将在不到 2 个月的时间内减少 10 分钟。【参考方案2】:

执行此操作的简单方法是使用任务和一个简单的循环:

public async Task StartTimer(CancellationToken cancellationToken)


   await Task.Run(async () =>
   
      while (true)
      
          DoSomething();
          await Task.Delay(10000, cancellationToken);
          if (cancellationToken.IsCancellationRequested)
              break;
      
   );


当你想停止线程时,只需中止令牌:

cancellationToken.Cancel();

【讨论】:

1.不需要Task.Run。 2. DoSomething 应该是异步的。 3.你应该等待任务返回表单Task.Delay 此代码在await Task.Delay(...) 行抛出System.Threading.Tasks.TaskCanceledException 这不是周期性的,因为 DoSomething 的执行时间不为零。例如,如果 DoSomething 需要 500 毫秒并且每小时运行一次,那么您将在不到 2 个月的时间内减少 10 分钟。 @tymtam 如果 DoSomehthing 是异步的并且我们不等待它,那么它不会向我们的定期执行添加任何内容。唯一的问题是,如果 DoSomethings 花费的时间比我们的定期检查要长,这应该通过对 DoSomething 进行对象锁定来解决,以避免重新进入 关于@pitersmx 提到的任务TaskCanceledException,这是任务库的默认行为,每个任务都应该按照source 实现。您可以将 Task.Delay 包装在 try/catch 块中,也可以简单地忽略该任务上的取消令牌【参考方案3】:

这是一个以周期性方式调用异步方法的方法:

public static async Task PeriodicAsync(Func<Task> action, TimeSpan interval,
    CancellationToken cancellationToken = default)

    while (true)
    
        var delayTask = Task.Delay(interval, cancellationToken);
        await action();
        await delayTask;
    

每个interval 调用提供的action,然后等待创建的Task。等待的持续时间不会影响间隔,除非它碰巧比那个长。在这种情况下,不重叠执行的原则优先,因此将延长期限以匹配等待的持续时间。

如果出现异常,PeriodicAsync 任务将在失败的情况下完成,因此如果您希望它具有错误恢复能力,您应该在 action 中包含严格的错误处理。

使用示例:

Task statisticsUploader = PeriodicAsync(async () =>

    try
    
        await UploadStatisticsAsync();
    
    catch (Exception ex)
    
        // Log the exception
    
, TimeSpan.FromMinutes(5));

.NET 6 更新:现在可以通过使用新的PeriodicTimer 类来实现几乎相同的功能,而不会在每个循环上产生Task.Delay 分配的成本:

public static async Task PeriodicAsync(Func<Task> action, TimeSpan interval,
    CancellationToken cancellationToken = default)

    using var timer = new PeriodicTimer(interval);
    while (true)
    
        await action();
        await timer.WaitForNextTickAsync(cancellationToken);
    

WaitForNextTickAsync 方法返回一个ValueTask&lt;bool&gt;,这使得这个实现更加高效。不过,效率上的差异非常小。对于每 5 分钟运行一次的周期性操作,在每次迭代中分配一些轻量级对象的影响几乎为零。

基于PeriodicTimer 的实现的行为与基于Task.Delay 的实现不同。如果一个动作的持续时间长于interval,两个实现都会在前一个动作完成后立即调用下一个动作,但是基于PeriodicTimer的实现的调度器不会像Task.Delay那样向前滑动基于 - 的实现确实如此。请参阅下面的大理石图以直观地展示差异:

Clock          X---------X---------X---------X---------X---------X---------X--
Task.Delay:    +-----|   +---|     +------------|+---|     +------|  +--|
PeriodicTimer: +-----|   +---|     +------------|+---| +------|  +--|      +--

基于Task.Delay 的实现的调度被永久向前移动,因为action 的第三次调用比interval 持续的时间更长。

【讨论】:

嗯。这会比使用计时器性能更高还是更低? @sommmen 我相信性能方面应该是平等的,因为System.Timers.TimerTask.Delay 在内部都使用System.Threading.Timer

以上是关于以指定的时间间隔定期运行异步方法的主要内容,如果未能解决你的问题,请参考以下文章

在python中以特定间隔运行任务[重复]

Python线程以精确的时间间隔执行

setIntervalsetTimeoutrequestAnimationFrame

以时间间隔顺序运行 TestNG 测试

如何在固定时间间隔后使用 IntentService 实现进行轮询?

定时器参考