C# 从源码理解ValueTask.Result为何是非阻塞的

ValueTask本质是解决异步方法(Task)同步执行下的性能问题(频繁调用异步方法需要不断实例化Task对象)。

关于ValueTask的Result是否能阻塞的问题,答案是绝对的:不能阻塞。但有一些情况容易被人误解成ValueTask能够阻塞,看如下代码。

例如对于如下代码来说,ValueTask.Result是非阻塞的。如果channel中没有值,则会报错,此时需要先判断isCompleted,如果为false则需要asTask().Result阻塞等待。或使用await等待ValueTask完成(ValueTask实现了GetAwaiter,因此可以使用await实现阻塞)

{
    Channel<int> channel = Channel.CreateUnbounded<int>();
    int f = channel.Reader.ReadAsync().Result;     
}

对于如下代码来说,很多人会误以为ValueTask.Result是阻塞的,其实不然。

{
    int result = AsyncMethodTest().Result;
    Console.WriteLine(888);
    //最终输出结果是 999\n888
}


public async ValueTask<int> AsyncMethodTest(){
    await Task.Delay(1000);
    Console.WriteLine(999);
    return 1;
}

虽然上述代码的输出结果直观表明了:AsyncMethodTest()方法返回的ValueTask会在Result调用时阻塞,但这并不代表ValueTask是阻塞的。下面来看ValueTask.Result官方源码:

//.Net 7.0
    [DebuggerBrowsable(DebuggerBrowsableState.Never)] // prevent debugger evaluation from invalidating an underling IValueTaskSource<T>
        public TResult Result
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get
            {
                object? obj = _obj;        // 1
                Debug.Assert(obj == null || obj is Task<TResult> || obj is IValueTaskSource<TResult>);

                if (obj == null)    // 2
                {
                    return _result!;
                }

                if (obj is Task<TResult> t)    //3
                {
                    TaskAwaiter.ValidateEnd(t);
                    return t.ResultOnSuccess;
                }

                return Unsafe.As<IValueTaskSource<TResult>>(obj).GetResult(_token);    //4
            }
        }

1处的 _obj 是ValueTask对象的私有成员变量,其用于存储IValueTaskSource<TResult>、 Task<TResult>对象。ValueTask的公有构造函数有三种,分别是直接输入TResult值用于同步返回的、输入一个Task<TResult>对象用于包装的、输入一个IValueTaskSource<TResult>的。第一种也就对应着2处代码,表示无需等待直接返回;3处代码用于判断当前ValueTask是否是Task对象的包装,如果是则直接等待Task并返回结果,等价于return Task.Result,这也是为何前面所示的ValueTask.Result可以阻塞,具体原因后面再分析。4处代码则是表示自己构造的具有IValueTaskSource接口的对象,这种即ValueTask的非阻塞获取结果,也就是前面Channel.Reader.ReadAsync报错的原因,具体原因后面分析。

看到上述Result源码可以确定,既然async标识的异步方法返回的ValueTask调用Result能够阻塞,则可以推测代码执行到此方法的return时是返回了一个由ValueTask包装的Task对象。查看IL代码则能证实这一点,C#和IL代码如下:

//首先构造一个方法以供分析其IL代码
async ValueTask<int> T3(){
    await Task.Yield();
    return 1;
}
.method private hidebysig static 
        valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1<int32> T3 () cil managed 
    {
        .custom instance void [System.Runtime]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [System.Runtime]System.Type) = (
            01 00 16 54 65 73 74 32 2e 50 72 6f 67 72 61 6d
            2b 3c 54 33 3e 64 5f 5f 31 00 00
        )
        .custom instance void [System.Runtime]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x20c4
        // Header size: 12
        // Code size: 49 (0x31)
        .maxstack 2
        .locals init (
            [0] class Test2.Program/'<T3>d__1'
        )

        IL_0000: newobj instance void Test2.Program/'<T3>d__1'::.ctor()
        IL_0005: stloc.0
        IL_0006: ldloc.0
        IL_0007: call valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1<!0> valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1<int32>::Create()
        IL_000c: stfld valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1<int32> Test2.Program/'<T3>d__1'::'<>t__builder'
        IL_0011: ldloc.0
        IL_0012: ldc.i4.m1
        IL_0013: stfld int32 Test2.Program/'<T3>d__1'::'<>1__state'
        IL_0018: ldloc.0
        IL_0019: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1<int32> Test2.Program/'<T3>d__1'::'<>t__builder'
        IL_001e: ldloca.s 0
        IL_0020: call instance void valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1<int32>::Start<class Test2.Program/'<T3>d__1'>(!!0&)
        IL_0025: ldloc.0
        IL_0026: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1<int32> Test2.Program/'<T3>d__1'::'<>t__builder'
        IL_002b: call instance valuetype [System.Runtime]System.Threading.Tasks.ValueTask`1<!0> valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1<int32>::get_Task()
        IL_0030: ret
    } // end of method Program::T3

首先IL_0007处调用Create静态方法构造了一个ValueTask异步方法构造器,然后在IL_0020处调用构造器的Start方法开始一些关于上下文的处理。然后在IL_002b处调用构造器的get_Task()方法获取一个ValueTask并返回。下面来看get_Task()方法的IL代码

  // if (m_task == s_syncSuccessSentinel)
    IL_0000: ldarg.0
    IL_0001: ldfld class System.Threading.Tasks.Task`1<!0> valuetype System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1<!TResult>::m_task
    IL_0006: ldsfld class System.Threading.Tasks.Task`1<!0> valuetype System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1<!TResult>::s_syncSuccessSentinel
    IL_000b: bne.un.s IL_0019

    // return new ValueTask<TResult>(_result);
    IL_000d: ldarg.0
    IL_000e: ldfld !0 valuetype System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1<!TResult>::_result
    IL_0013: newobj instance void valuetype System.Threading.Tasks.ValueTask`1<!TResult>::.ctor(!0)
    IL_0018: ret

    // Task<TResult> task = m_task ?? (m_task = new Task<TResult>());
    IL_0019: ldarg.0
    IL_001a: ldfld class System.Threading.Tasks.Task`1<!0> valuetype System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1<!TResult>::m_task
    IL_001f: dup
    IL_0020: brtrue.s IL_0031

    // (no C# code)
    IL_0022: pop
    IL_0023: ldarg.0
    IL_0024: newobj instance void class System.Threading.Tasks.Task`1<!TResult>::.ctor()
    IL_0029: dup
    IL_002a: stloc.1
    IL_002b: stfld class System.Threading.Tasks.Task`1<!0> valuetype System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1<!TResult>::m_task
    // return new ValueTask<TResult>(task);
    IL_0030: ldloc.1

    IL_0031: stloc.0
    IL_0032: ldloc.0
    IL_0033: newobj instance void valuetype System.Threading.Tasks.ValueTask`1<!TResult>::.ctor(class System.Threading.Tasks.Task`1<!0>)
    IL_0038: ret

构造器中有一个私有变量Task<TResult> m_task,其值在此前并未进行赋值,因此是null。还有一个静态只读变量是s_syncSuccessSentinel,其值为new Task<TResult>(default(TResult)),IL_000b则是判断m_task与s_syncSuccessSentinel是否相等,除非该ValueTask在调用时能够返回TResult结果,否则其值为null,此时跳转到IL_0019,后续代码不难发现,最终构造器生成了一个Task实例并由ValueTask包装返回,也即印证了上面的猜测:async异步方法的ValueTask返回值实际是对Task对象的包装。下图通过C#代码的调试来印证这一观点:

首先,调试代码如下:
在这里插入图片描述
在这里插入图片描述
上图为代码和调试过程中监视的局部变量情况,图中两行蓝底的变量分别是async异步方法直接返回的ValueTask和由ValueTask包装后的async异步方法返回的Task,前面提到过,_obj私有变量在ValueTask中用于存储Task<TResult>或IValueTaskSource<TResult>对象,而从图中不难发现,两个ValueTask的_obj其类型均是AsyncTaskMethodBuilder。

至于前面提到的Channel.Reader.ReadAsync.Result为何是非阻塞的,因为其实现了IValueTaskSource<TResult>接口,也就是说在调用Result时其执行的是return Unsafe.As<IValueTaskSource<TResult>>(obj).GetResult(_token); 这段代码行,而这段代码行的GetResult是非阻塞的,这也就是为何ValueTask是非阻塞的。(注意,ValueTask存在的意义就是解决异步方法同步执行时Task带来的性能问题,如果用ValueTask对Task包装,虽然也能实现ValueTask的阻塞,但这并不能称为真正意义上的ValueTask,因为此ValueTask的存在并未消除Task在堆内存上产生的开销)。

关于Channel.Reader.ReadAsync.Result实现IValueTaskSource<TResult>接口的佐证材料:(代码行过多,这里只列出关键类和方法,且UnBounded与Bounded类似)

namespace System.Threading.Channels
{
    internal sealed class BoundedChannel<T> : Channel<T>, IDebugEnumerable<T>
    {
        private sealed class BoundedChannelReader : ChannelReader<T>, IDebugEnumerable<T>
        {
            public override ValueTask<T> ReadAsync(CancellationToken cancellationToken)
            {
                //前面一些异常处理、判断处理省略
                //最终返回的是一个Channel内部实现的实现了IValueTaskSource\<TResult\>接口的AsyncOperation对象的ValueTaskOfT
                var reader = new AsyncOperation<T>(parent._runContinuationsAsynchronously | cancellationToken.CanBeCanceled, cancellationToken);
                parent._blockedReaders.EnqueueTail(reader);
                return reader.ValueTaskOfT;        //ValueTaskOfT详见下面代码
            }
        }
    }

    internal partial class AsyncOperation<TResult> : AsyncOperation, IValueTaskSource, IValueTaskSource<TResult>
    {
        public ValueTask<TResult> ValueTaskOfT => new ValueTask<TResult>(this, _currentId);    //此处采用实现了IValueTaskSource<TResult>接口的实例对象构造        
                                                                                                //了一个ValueTask
    }

}

如果细看代码,可能有一个疑问,为何Channel.Reader.ReadAsync()中的var reader = new AsyncOperation<T>(…)又实例化了一个对象,这样的效果最终会抵消ValueTask取代Task带来的性能提升。其实上面的代码(在var reader = new Async…前面)为了方便理解省略了一个判断条件:

// If we're able to use the singleton reader, do so.
if (!cancellationToken.CanBeCanceled)
{
    AsyncOperation<T> singleton = _readerSingleton;
    if (singleton.TryOwnAndReset())
    {
        parent._blockedReaders.EnqueueTail(singleton);
        return singleton.ValueTaskOfT;
    }
}

从官方给的注释不难发现,上述代码采用的是AsyncOperation单例类,而这个单例类可以反复使用从而达到一次实例化反复使用的效果,只有在使用令牌并且异步能被取消的条件下才会new一个新的AsyncOperation类,通常情况下性能提升是可观的。

从上面所有的分析来看,我们自己编写的普通异步方法无论返回的是ValueTask还是Task,其都会生成一个Task对象,反而ValueTask还会额外进行一次封装,因此除非我们采用Channel的逻辑自己对相关异步方法进行封装并实现IValueTaskSource<TResult>接口,否则使用ValueTask来代替Task将会对性能起到反向作用。当然,使用ValueTask想要提升性能,意味该异步方法能频繁出现同步返回的情景,这通常只在IO缓冲区中出现(如Socket、文件IO等),而这些方法也都由底层系统API进行封装,只要是提供ValueTask的异步方法,如同Channel所做的那样,我们理应无需对此有所顾虑。正如微软官方所建议的那样:ValueTask是一个具有多个字段的结构,因此从方法返回它会导致复制更多的数据,而Task仅返回引用。因此对于任何不返回结果的异步方法,默认选择应为返回Task,仅当性能分析它值得时,ValueTask才应使用而不是Task。

引用一段官方解释来阐述为何ValueTask使用IValueTaskSource更加高效:ValueTask是一个结构,可以包装Task或IValueTaskSource实例。异步方法返回由ValueTask包装的IValueTaskSource实例,使高吞吐量应用程序能够避免使用可重用IValueTaskSource对象池进行分配。

但请注意,上面引文中所述即单例思想,这也就意味着其不能用于并发操作,仅能用于非并发访问的对象。Channel也是如此,虽然Channel可以多线程同时进行读取,但其内部实现采用了锁的机制,即在ReadAsync内部锁住AsyncOperation防止同一时间由多个线程对其进行访问。

最后展示一段代码的测试,来对比异步方法返回ValueTask和Task的性能开销

int k = 100000;
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (int i = 0; i < k; i++)
{
	ValueTask<int> t = T3();
}
stopwatch.Stop();
Console.WriteLine(stopwatch.Elapsed.ToString());

stopwatch.Restart();

for (int i = 0; i < k; i++)
{
	Task t = T2();
}
stopwatch.Stop();
Console.WriteLine(stopwatch.Elapsed.ToString());

结果如下:(粗略测试,测试结果并不稳定,但从整体上来看ValueTask性能略低于Task)

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值