以正确的方式实现异步“yielding”

mmi*_*mix 3 c# continuations task async-await

异步方法在调用者上下文/线程上运行同步,直到其执行路径遇到 I/O 或涉及一些等待的类似任务,然后它不等待,而是返回到原始调用者,稍后恢复其继续。问题是,实现“等待”方法的首选方式是什么。文件/网络/等异步方法是如何做到的?

假设我有一个方法,其中涉及一些等待,当前开箱即用的 IO 未涵盖这些等待。我不想阻止调用线程,也不想强迫我的调用者执行 a 来Task.Run()卸载我,我想要一个干净的异步/等待模式,以便我的调用者可以无缝集成我的库,并且我可以在其上下文上运行,直到这样我需要屈服的时候了。为了便于论证,让我们假设我想要创建一个未涵盖的新 IO 库,并且我需要一种方法来使所有保持异步的粘合在一起。

Task.Yield还要继续吗?我必须自己做Task.Run/Task.Wait等吗?两者看起来更像是相同的抽象(这带来了如何收益的问题Yield)。我很好奇,因为有很多关于 async/await 延续如何为消费者工作以及所有 IO 库如何准备就绪的讨论,但很少有人谈论实际的“突破”点如何工作以及流程制造商应如何实现它。同步路径末尾的代码如何实际释放控制以及该方法在该点和之后如何运行。

Mar*_*ell 6

如果您位于异步堆的底部,没有内置的异步下游调用可供遵循,那么:它就落到了您的身上。做到这一点的简单方法是为某些人分配一个(TCSTaskCompletionSource<T>) ,以任何你需要的方式T连接异步工作(不是Task<T>基于的),将 TCS 粘在你以后可以得到的地方,然后交回从 TCS 到呼叫者。当异步工作完成时 - 可能通过某种回调,或者任何适合该 API 的方式;从你填充的地方获取TCS,并通过等方式在那里发出完成信号。.TaskTrySetResult

不过,还有很多事情需要考虑:

  • 在许多情况下,如果“线程盗窃”是一个巨大的问题,您可能希望确保传递TaskCreationOptions.RunContinuationsAsynchronously给 TCS 构造函数(否则,会await窃取任何调用的线程.TrySetResult
  • 有多种方法可以创建和管理Task[<T>]实例而无需额外分配 a TaskCompletionSource<T>,但它们更高级
  • 或者在极端情况下,如果这是高吞吐量:ValueTask[<T>]有一个基于令牌的 API(通过IValueTaskSource[<T>]),允许多次使用相同的对象模型(作为不同的ValueTask[<T>]值),以避免任何额外的分配 - 同样,这是一个高级设想