【问题标题】: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 的回答为您提供了如何以火和忘记的方式调用异步的链接(也称为“从事件处理程序调用异步方法”) ...

标签: c# asp.net .net multithreading async-await


【解决方案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
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多