带有异步Task.Run方法的死锁以及来自Synchronus方法的等待和超时

时间:2019-07-13 07:20:45

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

我有一个方法定义为:

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

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

enter image description here 谁能建议我应对这种情况的最佳方法,以免出现任何死锁?

2 个答案:

答案 0 :(得分:5)

您由于未使用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超时构造函数重载(首选),使用其他更好的超时方法修复第一个死锁:

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线程上继续执行继续队列。 / p>

答案 1 :(得分:0)

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

您的代码应为异步/等待状态,并使用CancellationSource并处理SyncMethod()中的取消令牌。但是,如果不能,并且据我所知,您想要异步运行一个方法并过一会儿就将其强行杀死,则可能应该使用线程并中止它们。

警告:中止线程是不安全的,除非您知道自己在做什么,并且在以后的版本中甚至可能从.NET中删除它。

我实际上已经研究了一段时间:https://siderite.blogspot.com/2019/06/how-to-timeout-task-and-make-sure-it.html