【问题标题】:Method returning task behaving synchronously in C# console app在 C# 控制台应用程序中同步返回任务的方法
【发布时间】:2019-04-15 11:39:39
【问题描述】:

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

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 的第 2 行设置一个断点,对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 的内部结构,因为我意识到它们可能是问题的罪魁祸首。

【问题讨论】:

  • 在控制台应用程序中 SynchronizationContext 返回线程池。在 ASP.NET 和 Win Forms 中 - 它是同一个线程。这不再是 .NET 核心的情况,但对于 .NET 框架,您必须关心
  • 我认为您需要在 GetAllFooAsync 中进行 async/await - 同步操作在此方法中。 return await Task.FromResult(result);

标签: c# .net async-await task-parallel-library


【解决方案1】:

你已经得到了一些描述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);
}

所以现在我们有了使用同步 API 的同步实现。现在的问题是关于消费。如果您从 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 上)。

【讨论】:

    【解决方案2】:

    是不是因为GetAllFooAsync其实是同步的,而是模仿了一个异步的工作流程?

    是的,Task.FromResult 返回一个立即为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.

    【讨论】:

    • 仅使用 Task.Run 将同步转换为异步通常被认为是不好的做法,但在控制台应用程序中它不像 asp.net 那样糟糕。我可能会建议您启动真正的异步调用,运行同步操作,然后等待所有异步任务,因为看起来您不需要其结果作为其他操作的输入。
    • 谢谢你,很遗憾我没有“真正的异步调用”来回退,这就是为什么我的解决方案一开始就这样。
    【解决方案3】:

    asynckeyword 的存在不会使方法异步,它只是向编译器发出信号,将方法的代码转换为状态机类,该类可以与异步流一起使用。实际上,当一个方法执行异步操作(如 I/O、将工作卸载到另一个线程等)时,它就变成了异步的,在这种情况下,它由 3 部分组成:异步操作之前的同步部分,异步操作的调用启动操作并将控制权返回给调用线程,然后继续。在您的情况下,最后两部分不存在,因此您的调用是同步的。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-10-10
      • 2017-04-10
      • 1970-01-01
      • 2013-05-30
      • 2010-10-30
      • 1970-01-01
      • 2010-12-13
      • 1970-01-01
      相关资源
      最近更新 更多