以正确的方式实现异步 "yielding"

Implement async "yielding" the proper way

异步方法 运行s 在调用者 context/thread 上同步,直到它的执行路径 运行s 进入 I/O 或类似的任务,其中有一些等待,然后,而不是等待,它 returns 到原始呼叫者,稍后恢复其继续。问题是,实现该“等待”方法的首选方式是什么。 File/Network/etc 异步方法是如何做到的?

让我们假设我有一个方法,它会涉及一些等待,而当前的 IOs 开箱即用。我不想阻塞调用线程,也不想强迫我的调用者执行 Task.Run() 来卸载我,我想要一个干净的 async/await 模式,以便我的调用者可以无缝集成我的库和我可以在其上下文中 运行 直到我需要屈服为止。为了争论起见,假设我想制作一个未涵盖的新 IO 库,我需要一种方法来制作所有保持异步的胶水。

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

如果您是异步堆的底部,没有内置的异步下游调用可以推迟,那么:它落在您身上。 简单的方法是为一些T分配一个TaskCompletionSource<T>(TCS),连接异步工作(不是Task<T> based)以您需要的任何方式,将 TCS 粘贴到您稍后可以拿到的地方,然后将 .Task 从 TCS 返回给调用者。当异步工作完成时——可能通过某种回调,或任何适合的回调 API;从你塞满它的任何地方获取 TCS,并在那里通过 TrySetResult 等发出完成信号

虽然有很多事情需要考虑:

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