【问题标题】:C# Async when also needing support for synchronous callC# Async 当还需要支持同步调用时
【发布时间】:2020-06-09 13:22:37
【问题描述】:

我的情况是,我们有一些通过用户输入(按钮单击)运行的代码,这些代码通过一系列函数调用运行并导致生成一些数据(这是一个非常繁重的操作,需要几分钟) .我们想为此使用异步,这样它就不会在我们执行此操作时锁定 UI。

但同时我们也有一个要求,即这些函数也可以通过一个最好是同步的 API 来使用。

可视化/示例(伪代码):

public async void Button_Click() // from UI context
{
  await instanceOfClassA.FuncA();

  // some code
}

public async Task ClassA.FuncA()
{
  await instanceOfClassB.FuncB()

  // some code
}

public async Task ClassB.FuncB()
{
  await instanceOfClassC.SomeHeavyFunc()

  // some code
}

public async Task ClassC.SomeHeavyFunc()
{
  // some heavy calculations
}


// Also need to provide a public synchronous API function
public void SomeClass.SynchronousAPIFunc()
{
  // need to call differentInstanceOfClassB.FuncB()
}

有没有办法让公共 API 函数在内部等待异步操作?

编辑: 在此post 中,用户 Rachel 提供了该问题的两个答案。两者似乎都很有趣,但我不确定哪一个会提供最少的风险/副作用。

编辑2: 请注意,我们使用的是 .NET v4.6.1。

提前致谢。

【问题讨论】:

  • 在 SomeClass.SynchronousAPIFunc() 你可以调用 differentInstanceOfClassB.FuncB().Result();这将导致方法 FuncB 被同步调用
  • 这可以做到,但是很容易引入微妙的死锁。听起来你的要求很糟糕,如果不是荒谬的话
  • 在这种情况下,通常最好过渡到基于队列或分布式工作流范式。如果您的 API 可以运行一个函数几分钟,通常调用框架会超时或将其解释为无响应。如果这个过程真的需要那么长时间,那么就改用 pub-sub 风格的架构。
  • 最好从你的类中公开单独的异步/同步方法
  • @AluanHaddad 使用.GetAwaiter().GetResult() 具有相同的死锁风险,因此不再“安全”。它所做的只是解开异常。

标签: c# .net async-await


【解决方案1】:

制作只调用异步版本的方法的“同步”版本的问题在于it can cause deadlocks,特别是如果调用此代码的人不知道这是正在发生的事情。

如果你真的想制作同步版本,那就跟随微软的脚步,编写不使用任何异步代码的全新方法。例如,File.ReadAllLines() 的实现不使用任何与File.ReadAllLinesAsync() 相同的代码。

如果您不想这样做,那么就不要提供方法的同步版本。让调用者决定如何处理它。如果他们想在上面同步阻塞,那么他们可以降低死锁的风险。

【讨论】:

  • 感谢您的回复。我同意这将是最整洁的方法。尽管这些功能非常大并且涉及很多对象,但可能需要进行相当多的重组。函数调用链经过相当多的代码。但它可能值得重构,尽管说服其他人并不总是那么容易。
  • 我会将其标记为解决方案/答案,因为它是“代码气味”最少的解决方案。
【解决方案2】:

但同时我们也有一个要求,即这些函数也可以通过一个最好是同步的 API 来使用。

如果您需要同时公开同步和异步 API,我推荐boolean argument hack。这看起来像:

public Task<T> FuncBAsync() => FuncBAsync(sync: false);
public T FuncB() => FuncBAsync(sync: true).GetAwaiter().GetResult();
public async Task<T> FuncBAsync(bool sync)
{
  // Note: is `sync` is `true`, this method ***must*** return a completed task.
  ...
}

有没有办法让公共 API 函数在内部等待异步操作?

我不建议使用直接阻塞(例如,GetAwaiter().GetResult()),因为直接实现会lead to deadlocks

编辑:在这个post 中,用户 Rachel 提供了两个问题的答案。

强烈建议不要使用该解决方案。它使用带有自定义SynchronizationContext 的嵌套消息循环,但不执行COM 泵送。这可能会导致问题,尤其是从 UI 线程调用时。即使泵送不是问题,这种解决方案也可能导致意外的重入,这是无数、极其微妙且难以发现的错误的根源。

【讨论】:

  • 谢谢!结合我标记为解决方案的答案(提供同步和异步功能),这似乎非常有趣。虽然我现在确实有一个问题,因为我阅读了这篇文章和你链接的文章。我看到的大多数异步函数链接示例在链中所有函数的签名中使用 async (并在所有主体中等待),而您只将它用于最里面的调用。如果在链的上游省略了 async 关键字,执行是否仍会返回给调用者?到时候只返回任务就足够了吗?
  • 抱歉不清楚。您将需要在整个链条中一直使用此模式。每个可以同步或异步的方法都需要使用这种模式。
  • 感谢您的回复!对不起,我觉得我的措辞有点糟糕。我只是想知道为什么您的示例中的第一行没有说“public async Task FuncBAsync() => await FuncBAsync(sync: false);”。我不清楚为什么我们不在该行使用 async 和 await 关键字。我也要去看看你关于这件事的书。
  • 是的,当它只是一个简单的传递时,我确实倾向于elide async/await
【解决方案3】:

您可以使用.GetAwaiter().GetResult()

根据您的示例,它看起来像:

public void SomeClass.SynchronousAPIFunc()
{
  // need to call differentInstanceOfClassB.FuncB()
  ClassB.FuncB().GetAwaiter().GetResult();
}

另外,关于何时不使用上述内容的一个很好的参考可以在Dont Block on Async Code找到

【讨论】:

  • 阻塞代码绝对不应该隐藏在 API 方法中。
  • 这在某些情况下可能会死锁,这是一种危险的方法。如果你真的需要两个实现,通常的方法是简单地编写两个实现。
  • 感谢您的回复!这是我最初采用的方法,但我已经阅读了你链接的文章,我开始变得冷漠。
猜你喜欢
  • 1970-01-01
  • 2017-05-15
  • 2014-01-08
  • 2017-01-13
  • 1970-01-01
  • 1970-01-01
  • 2016-10-20
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多