方法返回在 C# 控制台应用程序中同步运行的任务

Method returning task behaving synchronously in C# console app

在 C# 控制台应用程序中,我有一个带有几个异步方法的存储库 class:

public class SomeRepo
{
   internal Task<IList<Foo>> GetAllFooAsync() 
   { 
       // this is actually fake-async due to legacy code.
       var result = SomeSyncMethod();
       return Task.FromResult(result);
   }

   public Task<IList<Foo>> GetFilteredFooAsync()
   {             
       var allFoos = await GetAllFooAsync().ConfigureAwait(false);
       return allFoos.Where(x => x.IsFiltered);
   }
}

Program.cs中:

var someRepo = new SomeRepo();
var filteredFoos = someRepo.GetFilteredFooAsync(); // no await
// a couple of additional async calls (to other classes) without await..
// .. followed by:
await Task.WhenAll(filteredFoos, otherTask, anotherTask).ConfigureAwait(false);

令我困惑的是,如果我在 Program.cs 的第二行放置一个断点,对 someRepo.GetFilteredFooAsync() 的调用不会继续到下一行,而是卡住直到操作完成(就好像它是同步的)。而如果我将对 GetAllFooAsync(在 GetFilteredFooAsync 中)的调用更改为包含在 Task.Run:

public class SomeRepo
{
   internal Task<IList<Foo>> GetAllFooAsync() { // ... }

   public Task<IList<Foo>> GetFilteredFooAsync()
   {             
       var allFoos = await Task.Run(() => GetAllFooAsync).ConfigureAwait(false);
       return allFoos.Where(x => x.IsFiltered);
   }
}

..操作以这种方式按预期进行。是不是因为GetAllFooAsync其实是同步的,只是模仿了一个异步的工作流程?

编辑:改写了标题并添加了 GetAllFooAsync 的内部结构,因为我意识到它们可能是问题的罪魁祸首。

Is it because GetAllFooAsync is actually synchronous, but imitating an asynchronous workflow?

是的,Task.FromResult returns 任务立即 RanToCompletion 所以它是同步的。人们经常忘记,在某些情况下,任务在返回时可能已经完成,因此不会 运行 异步。

This method creates a Task object whose Task.Result property is result and whose Status property is RanToCompletion. The method is commonly used when the return value of a task is immediately known without executing a longer code path. The example provides an illustration.

async 关键字的存在不会使方法异步,它只是向编译器发出信号,将方法的代码转换为状态机 class,该状态机已准备好与异步流一起使用。实际上,一个方法在执行 I/O 等异步操作时变为异步方法,将工作卸载到另一个线程等,在这种情况下,它由 3 部分组成:异步操作之前的同步部分,异步调用启动操作的操作和 returns 对调用线程的控制,以及一个延续。在您的情况下,最后两部分不存在,因此您的调用是同步的。

您已经得到了一些很好的答案来描述 how async doesn't make things asynchronous, how async methods begin executing synchronously, and how await can act synchronously if its task is already completed

所以,让我们谈谈设计。

internal Task<IList<Foo>> GetAllFooAsync() 
{ 
  // this is actually fake-async due to legacy code.
  var result = SomeSyncMethod();
  return Task.FromResult(result);
}

如前所述,这是一个同步方法,但它有一个异步签名。这令人困惑;如果该方法不是异步的,那么使用同步的 API:

会更好
internal IList<Foo> GetAllFoo()
{ 
  // this is actually fake-async due to legacy code.
  var result = SomeSyncMethod();
  return result;
}

与调用它的方法类似:

public IList<Foo> GetFilteredFoo()
{             
  var allFoos = GetAllFoo();
  return allFoos.Where(x => x.IsFiltered);
}

所以现在我们有了同步 APIs 的同步实现。现在的问题是关于消费。如果你从 ASP.NET 消费这个,我建议同步消费它们。但是,如果您从 GUI 应用程序使用它们,则可以使用 Task.Run 将同步工作卸载到线程池线程,然后将其视为异步:

var someRepo = new SomeRepo();
var filteredFoos = Task.Run(() => someRepo.GetFilteredFoo()); // no await
// a couple of additional async calls (to other classes) without await..
// .. followed by:
await Task.WhenAll(filteredFoos, otherTask, anotherTask).ConfigureAwait(false);

具体来说,我推荐使用Task.Run来实现,例如GetAllFooAsyncYou should use Task.Run to call methods, not to implement themTask.Run 主要用于从 GUI 线程调用同步代码(即,不在 ASP.NET 上)。