15.5.6 【Task实现细节】跟踪栈
谈到栈帧(stack frame)时,可能会想到在方法中声明的局部变量。当然,可能还会注意到 一些隐藏的局部变量,如 foreach 循环中的迭代器。但栈上的内容不止这些,至少逻辑上是这样 。 很多情况下,在一些表达式还没有计算出来前,另一些中间表达式是不能使用的。最简单的例子 莫过于加法等二进制操作和方法调用了。 举个极简单的例子,思考下面这一行:
var x = y * z;
在基于栈的伪代码中,将为如下形式:
push y
push z
multiply
store
现在假设有如下 await 表达式:
var x = y * await z;
在等待 z 之前,需计算 y 并将其保存至某处,但可能会从 MoveNext() 方法立即返回,因此需 要一个逻辑栈来存储 y 。在执行后续操作时,可以重新存储该值,然后执行乘法。在这种情况下, 编译器可将 y 的值赋值给 stack 实例变量。这会引起装箱,但同时也意味着可以使用单个变量。 这是个简单的例子。假设有多个值需要存储,如下所示:
Console.WriteLine("{0} :{1}", x, await task);
在逻辑栈上需要存储格式化的字符串和 x 值。此时编译器会创建一个包含两个值的Tuple<string, int> ,并将其存储在 stack 的引用上。和 awaiter 一样,同一时间只需要一个逻辑栈,因此一直使用相同的变量是没有问题的。在后续操作中,可以从元组(tuple)中获取
实参,并用于方法调用。可下载的源代码中包含了完整的反编译示例,其中包括以上两条语句( LogicalStack.cs 和 LogicalStackDecompiled.cs )。
第二条语句最终将使用以下代码:
1 string localArg0 = "{0} {1}"; 2 int localArg1 = x; 3 localAwaiter = task.GetAwaiter(); 4 if (localAwaiter.IsCompleted) 5 { 6 goto SecondAwaitCompletion; 7 } 8 var localTuple = new Tuple<string, int>(localArg0, localArg1); 9 stack = localTuple; 10 state = 1; 11 awaiter = localAwaiter; 12 builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); 13 doFinallyBodies = false; 14 return; 15 SecondAwaitContinuation: 16 localTuple = (Tuple<string, int>) stack; 17 localArg0 = localTuple.Item1; 18 localArg1 = localTuple.Item2; 19 stack = null; 20 localAwaiter = awaiter; 21 awaiter = default(TaskAwaiter<int>); 22 state = -1; 23 SecondAwaitCompletion: 24 int localArg2 = localAwaiter.GetResult(); 25 Console.WriteLine(localArg0, localArg1, localArg2);
此处加粗显示的是与逻辑栈元素相关的代码。 目前所有需了解的内容均已介绍完毕。如果你跟上了我们的步伐,就肯定比99%的开发者更 为了解背后的细节。第一次没能完全理解也没有关系。在阅读这些状态机代码时,如果感到无法 理清头绪,可以暂时放松一下,过一会儿再来继续学习。