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

Ead*_*del 16 .net c# asp.net multithreading async-await

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

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

void MethodName(object e, ElapsedEventArgs args)
Run Code Online (Sandbox Code Playgroud)

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

async Task MethodName(object e, ElapsedEventArgs args)
Run Code Online (Sandbox Code Playgroud)

任何人都可以建议如何解决所描述的任务?任何提示赞赏.

The*_*ias 27

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

public static async Task PeriodicAsync(Func<Task> action, TimeSpan interval,
    CancellationToken cancellationToken = default)
{
    while (true)
    {
        Task delayTask = Task.Delay(interval, cancellationToken);
        await action();
        await delayTask;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

如果出现异常,PeriodicAsync任务将失败完成,因此如果您希望它能够容错,您应该在action.

使用示例:

Task statisticsUploader = PeriodicAsync(async () =>
{
    try
    {
        await UploadStatisticsAsync();
    }
    catch (Exception ex)
    {
        // Log the exception
    }
}, TimeSpan.FromMinutes(5));
Run Code Online (Sandbox Code Playgroud)

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

public static async Task PeriodicAsync(Func<Task> action, TimeSpan interval,
    CancellationToken cancellationToken = default)
{
    using PeriodicTimer timer = new(interval);
    while (true)
    {
        await action();
        await timer.WaitForNextTickAsync(cancellationToken);
    }
}
Run Code Online (Sandbox Code Playgroud)

WaitForNextTickAsync方法返回 a ValueTask<bool>,这使得该实现更加高效。但效率上的差异非常微小。对于每 5 分钟运行一次的定期操作,在每次迭代中分配一些轻量级对象的影响实际上应该为零。

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

public static async Task PeriodicAsync(Func<Task> action, TimeSpan interval,
    CancellationToken cancellationToken = default)
{
    while (true)
    {
        Task delayTask = Task.Delay(interval, cancellationToken);
        await action();
        await delayTask;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

正如您所看到的,PeriodicTimer并不能保证调用之间的最小间隔action

出于教育目的,上述实现PeriodicAsync故意保持简单。诸如参数验证之类的事情ConfigureAwait已被省略。


i3a*_*non 24

async当量是一个while带环Task.Delay(其在内部使用System.Threading.Timer):

public async Task PeriodicFooAsync(TimeSpan interval, CancellationToken cancellationToken)
{
    while (true)
    {
        await FooAsync();
        await Task.Delay(interval, cancellationToken)
    }
}
Run Code Online (Sandbox Code Playgroud)

传递一个CancellationToken很重要,这样你可以在需要时停止该操作(例如当你关闭你的应用程序时).

现在,虽然这通常与.Net相关,但在ASP.Net中,做任何类型的火灾都会很危险.有几个解决方案(如HangFire),其中一些记录在Spark 和Forget on ASP.NET上由Stephen Cleary撰写,其他人在如何在ASP.NET中运行后台任务由Scott Hanselman

  • 这不是周期性的,因为 `await FooAsync` 将有一个非零的执行时间。例如,如果 `FooAsync` 需要 500 毫秒并且每小时运行一次,那么你将在不到 2 个月的时间内关闭 10 分钟。 (8认同)
  • 我应该写"等待PeriodicFooAsync"还是只写"var result = PeriodicFooAsync"?在这种情况下我们需要等待吗? (3认同)
  • @Arvand,这取决于您是否要等待操作完成。在你的情况下,“result”只会保存任务,之后的下一行将立即发生。`await` 将异步“阻塞”,直到周期性任务被取消。 (2认同)
  • @Arvand您可能想将该任务保存在某个地方而不等待它(假设它永远存在直到您的应用程序关闭),然后您可以在关闭之前等待它。 (2认同)
  • 改进建议:您可以通过在等待“FooAsync”之前创建“Task.Delay”任务并在之后等待它来获得更一致的间隔。如果“FooAsync”在返回“Task”之前同步执行一些重要的工作,这将会产生影响。 (2认同)

Guy*_*vin 5

简单的方法是使用Tasks和一个简单的循环:

public async Task StartTimer(CancellationToken cancellationToken)
{

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

}
Run Code Online (Sandbox Code Playgroud)

当您要停止线程时,只需中止令牌:

cancellationToken.Cancel();
Run Code Online (Sandbox Code Playgroud)

  • 这不是周期性的,因为DoSomething的执行时间将为非零。例如,如果DoSomething需要500毫秒并且每小时运行一次,则在不到2个月的时间内您将关闭10分钟。 (6认同)
  • 1.不需要Task.Run。2. DoSomething应该是异步的。3.您应该等待从Task.Delay返回的任务。 (3认同)