【问题标题】:Deadlock with async Task.Run method with Wait from Synchronus method and timeout带有异步 Task.Run 方法的死锁,带有来自 Synchronus 方法的 Wait 和超时
【发布时间】:2019-11-22 19:33:09
【问题描述】:

我有一个方法定义为:

public Task<ReturnsMessage> Function() {
var task = Task.Run(() =>
{
     var result = SyncMethod();
     return new ReturnMessage(result);
});

 if (task.Wait(delay)) {
           return task;
}

  var tcs = new TaskCompletionSource<ReturnMessage>();
            tcs.SetCanceled();
            return tcs.Task;
}

现在根据 maxAttempts 值循环调用:

(方法名RetryableInvoke)

 for (var i = 0; i < maxAttempts; i++)
 {
    try
    {
        return Function().Result;
    }
    catch (Exception e)
    {
    }
  }

它工作得非常好,但是当负载很大时,我发现线程急剧增加并且转储向我显示以下警告:

谁能建议我处理这种情况的最佳方法,以免我看到任何类型的死锁?

【问题讨论】:

  • 最好的方法?一直使用基于任务的方法,因此您不必使用 .Wait 或 .Wait 进行阻塞。结果
  • 谢谢@PeterBons 我创建任务的原因是我想超时对下游系统的调用,而我知道的唯一方法是使用取消令牌或等待(withDelay)。即使我不使用 Wait 我也必须使用 .Result 因为响应将返回到同步方法(没有异步)。
  • @codebased 当使用异步时,然后异步执行 一路 - 期间
  • @codebased BTW 你的Function 方法是几乎所有“不要做异步”建议的混合。它似乎是异步的,但它是完全同步的。你为什么要返回一个任务呢?
  • syncmethod 里面是什么?

标签: c# .net asp.net-web-api async-await deadlock


【解决方案1】:

您正在使用 Task.Run 启动任务,然后如果它们超时,您将返回取消,但您永远不会停止任务。它们只是继续在后台运行。

您的代码应该是 async/await 并使用 CancellationSource 并在 SyncMethod() 中处理取消令牌。但是,如果您不能,并且您希望,按照我的理解,异步运行一个方法并在一段时间后强行终止它,您可能应该使用线程并中止它们。

警告:除非您知道自己在做什么,否则中止线程是不安全的,甚至可能在未来的版本中从 .NET 中删除。

其实我之前研究过这个:https://siderite.dev/blog/how-to-timeout-task-and-make-sure-it.html

【讨论】:

    【解决方案2】:

    您正在使应用程序死锁,因为您没有使用 async/awaitConfigureAwait(false),而是选择使用 Task.WaitTask.Result

    您首先应该知道Task.Run 捕获了它所执行的线程的SynchronizationContext。然后Task.Run 在新的ThreadPool 线程上运行。完成后,它将返回父线程以继续执行剩余的代码。返回时将返回捕获的SyncronizationContext。您使用Task.WaitTask.Result 打破了这个概念。两个Task 成员都会同步调用Task,这意味着父线程将自己阻塞,直到子线程完成。子线程完成但Task无法返回捕获的SynchronizationContext执行剩余代码(Task.Wait之后的代码),因为父线程仍然在等待任务运行完成而阻塞自己.

    因为你在一个地方使用了Task.Wait,在另一个地方使用了Task.Result,你已经造成了两种潜在的死锁情况:

    让我们单步执行Function() 代码:

    public Task<ReturnsMessage> Function() {
    

    1) 创建一个任务并启动它:

    var task = Task.Run(
      () => 
      { 
        var result = SyncMethod();
        return new ReturnMessage(result);
       });
    

    这里确实发生了重要的事情:
    Task.Run 捕获当前的SynchronizationContext 并在父线程继续执行时在后台开始执行。 (如果在这里使用了await,那么await 后面的剩余代码将被排入一个继续队列以供以后执行。目的是当前线程可以返回(离开当前上下文),以便它不需要等待和阻塞。一旦子线程运行完成,剩余的代码将由Task 执行,因为它之前已在继续队列中排队。

    2) task.Wait() 等待后台线程完成。等待意味着阻止线程继续执行。调用堆栈已停放。这等于后台线程的同步,因为父线程不再继续执行而是阻塞,因此不再并行执行:

     // Dangerous implementation of a timeout for the executing `task` object
     if (task.Wait(delay)) {
         return task;
     }
    

    这里确实发生了重要的事情:
    task.Wait() 阻塞当前线程 (SynchronizationContext) 等待子线程完成。子任务完成,Task 尝试从捕获的SynchronizationContext 中的延续队列中执行先前入队的剩余代码。但是这个上下文被等待子任务完成的线程阻塞了。潜在的僵局情况一。

    以下剩余代码将无法访问:

    var tcs = new TaskCompletionSource<ReturnMessage>();
    tcs.SetCanceled();
    return tcs.Task;
    

    asyncawait 被引入以摆脱阻塞等待。 await 允许父线程返回并继续。 await 之后的剩余代码将在捕获的SynchronizationContext 中作为延续执行。

    这是对第一个死锁的修复,也使用了使用Task.WhenAny(非首选)的适当任务超时解决方案:

    public async Task<ReturnsMessage> FunctionAsync()
    {
      using (var cancellationTokenSource = new CancellationTokenSource())
      {
        try
        {
          var task = Task.Run(
            () =>
            {
              // Check if the task needs to be cancelled
              // because the timeout task ran to completion first
              cancellationToken.ThrowIfCancellationRequested();
    
              var result = SyncMethod();
              return result;
            }, cancellationTokenSource.Token);
    
          int delay = 500;
          Task timoutTask = Task.Delay(delay, cancellationTokenSource.Token);
          Task firstCompletedTask = await Task.WhenAny(task, timoutTask);
    
          if (firstCompletedTask == task)
          {
            // The 'task' has won the race, therefore
            // cancel the 'timeoutTask'
            cancellationTokenSource.Cancel();
            return await task;
          }
        }
        catch (OperationCanceledException)
        {}
    
        // The 'timeoutTask' has won the race, therefore
        // cancel the 'task' instance
        cancellationTokenSource.Cancel();
    
        var tcs = new TaskCompletionSource<string>();
        tcs.SetCanceled();
        return await tcs.Task;
      }
    }
    

    或者使用CancellationTokenSouce timeout 构造函数重载(首选),使用替代的更好的超时方法来修复第一个死锁:

    public async Task<ReturnsMessage> FunctionAsync()
    {
      var timeout = 50;
      using (var timeoutCancellationTokenSource = new CancellationTokenSource(timeout))
      {
        try
        {
          return await Task.Run(
            () =>
            {
              // Check if the timeout elapsed
              timeoutCancellationTokenSource.Token.ThrowIfCancellationRequested();
    
              var result = SyncMethod();
              return result;
            }, timeoutCancellationTokenSource.Token);
        }
        catch (OperationCanceledException)
        {
          var tcs = new TaskCompletionSource<string>();
          tcs.SetCanceled();
          return await tcs.Task;
        }
      }
    }
    

    第二个潜在的死锁代码是Function()的消耗:

    for (var i = 0; i < maxAttempts; i++)
    {
      return Function().Result;
    }
    

    来自Microsoft Docs

    访问属性的 [Task.Result] get 访问器会阻塞调用线程,直到异步操作完成; 相当于调用Wait方法

    死锁的原因和前面解释的一样:一个阻塞的SynchronizationContext,它阻止了计划的继续执行。
    要修复第二个死锁,我们可以使用 async/await(首选)或ConfigreAwait(false):

    for (var i = 0; i < maxAttempts; i++)
    {
      return await FunctionAsync();
    }
    

    ConfigreAwait(false)。这种方法可用于强制同步执行异步方法:

    for (var i = 0; i < maxAttempts; i++)
    {
      return FunctionAsync().ConfigureAwait(false).GetAwaiter().GetResult();
    }
    

    ConfigreAwait(false) 指示Task 忽略捕获的SynchronizationContext 并继续在另一个永远不会是父线程的ThreadPool 线程上执行延续队列。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-03-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-27
    • 1970-01-01
    相关资源
    最近更新 更多