Skip to main content
 首页 » 编程设计

c#之带有异步 Task.Run 方法的死锁,带有来自 Synchronus 方法的 Wait 和超时

2024年01月29日24qlqwjy

我有一个方法定义为:

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

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


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

请您参考如下方法:

您正在使应用程序死锁,因为您没有使用 async/await或 ConfigureAwait(false ) 但您选择使用 Task.Wait和 Task.Result反而。

你应该首先知道Task.Run捕获 SynchronizationContext执行它的线程。然后Task.Run在新的 ThreadPool 上运行线。完成后,它将返回父线程以继续执行剩余的代码。返回时会返回捕获的SyncronizationContext .您使用 Task.Wait 打破了这个概念。和 Task.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; 
async和 await被引入以摆脱阻塞等待。 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 :

Accessing the property's [Task.Result] get accessor blocks the calling thread until the asynchronous operation is complete; it is equivalent to calling the Wait method.



死锁的原因和前面解释的一样:一个阻塞的 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 上继续执行延续队列永远不会是父线程的线程。