从同步代码调用异步 I/O 方法

Calling asynchronous I/O method from sync code

我有这段代码(同步方式):

// ...
var processingTasks = Enumerable.Range(0, count)
    .Select(x => Task.Run(async () =>    
      await CallApiAndUpdateSthAsync(accounts[x], cancellationToken)))
    .ToList();

var processingTasksAggregate = Task.WhenAll(processingTasks);

Task.WaitAny(globalStatusMonitoringTask, processingTasksAggregate);

我对这部分特别感兴趣:

Task.Run(async () => await CallApiAndUpdateSthAsync(accounts[x], cancellationToken))

如果我删除 Task.Run 会有什么不同(从可能的死锁角度来看)吗? IE。更改为:

CallApiAndUpdateSthAsync(accounts[x], cancellationToken)

我想删除它,因为在线程池上排队任务(通过 Task.Run)似乎是多余的,但我的同事建议我不要这样做,因为可能会出现死锁

整个方法运行作为类似于Hangfire的后台作业:

[编辑 1]:这是一个 .NET Framework(非核心)应用程序。

如果它是一个 asp.net core 应用程序(不仅仅是老派 asp.net),则没有任务死锁问题。 SynchronizationContext(没有 ConfigureAwait(false) 会导致死锁)在 asp.net core 中为 null。 There isn’t a SynchronizationContext in ASP.NET Core.

同样没有Task.Run - CallApiAndUpdateSthAsync将在你当前的线程上执行,直到第一个await里面。使用 Task.Run - 整个 CallApiAndUpdateSthAsync 将从您的主要执行中执行。您可以通过在 CallApiAndUpdateSthAsync.

的开头添加 Task.Yield 来修复它

Would it make a difference (from a possible deadlock perspective) if I removed the Task.Run?

可能吧。 common deadlock(我的博客 link)分为两部分:

  1. 阻塞异步代码。您的代码使用 Task.WaitAny.
  2. 执行此操作
  3. 一次只允许一个线程的上下文。

既然你的代码明显有(1),那么你可以断定它有(2)就会死锁。可能导致死锁的常见上下文包括 ASP.NET pre-Core 和 GUI 线程上下文。如果您的应用程序没有一次一个线程的上下文,那么就不会出现死锁。

I wanted to remove it, because queueing the task on a thread pool (via Task.Run) seemed redundant

不是真的。 “异步”并不意味着“运行 在不同的线程上”,因此两者并不多余。问题是删除 Task.Run 是否可行。可能会,也可能不会。如果此代码在单线程上下文中为 运行,则删除 Task.Run 将导致死锁。如果 CallApiAndUpdateSthAsync 可能 运行 同步(即,由于缓存命中),那么删除 Task.Run 将导致调用 运行 串行而不是并行,这可能也不理想。