ValueTask<TResult> 和异步状态机

7
根据文档ValueTask<TResult>是一个值类型,它封装了一个Task<TResult>和一个TResult,只有其中之一被使用。
我的问题是关于当遇到async关键字时,C#编译器生成的状态机是否足够智能,以便在结果立即可用时生成封装TResultValueTask<TResult>,或者在结果在经过await后才可用时生成封装Task<TResult>ValueTask<TResult>。以下是一个示例:
static async ValueTask<DateTime> GetNowAsync(bool withDelay)
{
    if (withDelay) await Task.Delay(1000);
    return DateTime.Now;
}

static void Test()
{
    var t1 = GetNowAsync(false);
    var t2 = GetNowAsync(true);
}

调用GetNowAsync(false)应返回TResult包装器,因为没有等待任何内容,而调用GetNowAsync(true)应返回Task<TResult>包装器,因为Task.Delay在结果可用之前被等待。 我担心状态机始终返回Task包装器,这将使ValueTask类型的所有优势无效(并保留所有劣势)。就我所知,ValueTask<TResult>类型的属性没有提供关于其内部包装内容的指示。 我在sharplab.io上复制了上面的代码,但输出也没有帮助我回答这个问题。

编译器不会更改异步方法的签名,因此如果在您的代码中它返回 ValueTask,则在状态机生成后它将继续返回它。它没有足够的空间来变得“聪明”。 - Dmytro Mukalov
@DmytroMukalov 你说得对。我的问题是关于每种情况下返回的 ValueTask 中包含了什么。 - Theodor Zoulias
如果您在询问,Task 类型不会从 Task.Delay 中泄漏,始终会返回 ValueTask<DateTime>。每个异步方法都由自己的状态机表示,该状态机公开特定的等待器类型,将由更高级别的消费者使用,而 Task.Delay 公开 Task 对状态机周围的 GetNowAsync 没有影响 - 它只知道如何获取 Task 等待器以及如何使用等待器异步地获取结果。 - Dmytro Mukalov
@DmytroMukalov 我认为你没有理解我的问题。你是否熟悉 ValueTask?如果不熟悉,这里有一个很好的介绍:了解 ValueTask 的原因、内容和时间。由 Task.Delay 返回的类型对我的问题没有影响。我询问的是在调用 async 方法时可能发生的 await 影响(或不影响)。 - Theodor Zoulias
我的答案重点是,在异步方法内部遇到的任何await都是其状态机的内部部分,并且对状态机的公共部分没有影响,即它返回的等待者。换句话说,await发生与否的事实仅对在状态机内获取结果的方式产生影响,但结果始终由其公共等待者(在您的情况下为ValueTask)公开。 - Dmytro Mukalov
@DmytroMukalov 我理解你的意思并且同意。我的问题与状态机返回的ValueTask值的非公共部分有关,具体来说是它们内部包装了什么,因为这会影响性能。我的基准测试显示,包装TResultValueTask创建和等待的速度比包装Task<TResult>ValueTask快3倍。顺便说一句,这给了我一个关于如何回答我的问题的想法! - Theodor Zoulias
2个回答

3
我想我应该回答自己的问题,因为我现在知道答案了。答案是我的担忧没有必要:C#编译器足够智能,可以在每种情况下发出正确类型的ValueTask<TResult>。当结果同步可用时,它会发出值包装器,否则就是任务包装器。

我通过性能测量得出了这个结论:通过测量每种情况下分配的内存和创建相同数量的任务所需的时间。结果清晰而一致。例如,当ValueTask<int>包装一个int值时,它消耗的内存恰好为12字节,当它包装Task<int>时,则正好为48字节,因此对于内部实现没有任何疑问。


1
很高兴听到这是答案。如果理解ValueTask的内部工作实际上是使用Linq.Async有效的必要条件,那么整个API将会存在严重缺陷。 - StackOverthrow

-1

编译器足够聪明,能够按照指令执行:

https://source.dot.net/#System.Private.CoreLib/shared/System/Threading/Tasks/ValueTask.cs,409

[AsyncMethodBuilder(typeof(AsyncValueTaskMethodBuilder<>))]
[StructLayout(LayoutKind.Auto)]
public readonly struct ValueTask<TResult> : IEquatable<ValueTask<TResult>>

注意使用 ValueTask:


顺便提一下,“小心”不是正确的处理这种类型的方式。ValueTask将被核心库(如Linq.Async)大量使用,因此理解它至关重要。 - Theodor Zoulias
@TheodorZoulias,如果你正在使用async foreach,那么你并不是直接与ValueTask交互。但是,如果你直接与IAsyncEnumerable交互,请注意ValueTask - Paulo Morgado
我不需要担心我理解的工具,而是那些我不理解的工具。这就是这个问题的要点,要理解这个工具。 - Theodor Zoulias
那么缺少什么呢? - Paulo Morgado
1
我的问题还没有得到答案。我现在知道了,如果没有其他人回答,我可能会发布它。 - Theodor Zoulias

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接