混合使用Task.Run和async/await关键字时编译器行为奇怪

3

请看以下C#代码

var L1 =
Task.Run(() =>
{
    return Task.Run(() =>
    {
        return Task.Run(() =>
        {
            return Task.Run(() =>
            {
                return new Dummy();
            });
        });
    });
});

var L2 =
Task.Run(async () =>
{
    return await Task.Run(async () =>
    {
        return await Task.Run(async () =>
        {
            return await Task.Run(async () =>
            {
                return new Dummy();
            });
        });
    });
});

var L3 =
Task.Run(async () =>
{
    return Task.Run(async () =>
    {
        return Task.Run(async () =>
        {
            return Task.Run(async () =>
            {
                return new Dummy();
            });
        });
    });
});


var r1 = L1.Result;
var r2 = L2.Result;
var r3 = L3.Result;


======================================================================
乍一看,L1、L2和L3都像是Task<Task<Task<Task<Dummy>>>>
结果,L1和L2只是简单的Task<Dummy>
所以,我查阅了MSDN关于Task.Run的资料
(我的重载Task.Run为:Task.Run<TResult>(Func<Task<TResult>>)
MSDN中说:

返回值为:表示函数返回的Task(TResult)的代理的Task(TResult)。

在备注中还指出:

Run<TResult>(Func<Task<TResult>>)方法由语言编译器用来支持async和await关键字。它不适合直接从用户代码中调用。


看起来,我不应该在我的代码中使用这个重载的Task.Run,
如果我这样做,编译器将剥离多余的Task<Task<Task<...<TResult>>>>层级,并简单地给出一个Task<TResult>
如果你将鼠标悬停在L1和L2上,它会告诉你它是Task<Dummy>,而不是我预期的Task<Task<Task<Task<Dummy>>>>

到目前为止还好,直到我看到L3
L3几乎与L1和L2完全相同。区别在于:
L3只有async关键字,没有await,而L1和L2则一个没有,一个有

令人惊讶的是,现在L3被视为Task<Task<Task<Task<Dummy>>>>,而L1和L2都被视为Task<Dummy>

我的问题:
1.
是什么导致编译器将L3与L1和L2区别对待?为什么简单地添加'async'到L1(或从L2中删除await)会导致编译器将其区别对待?


2. 如果您将更多的Task.Run级联到L1/L2/L3上,Visual Studio就会崩溃。我正在使用VS2013,如果我级联5个或更多层的Task.Run,它就会崩溃。 4个层是我能得到的最好结果,这就是为什么我以4个层作为我的示例。这只发生在我身上吗?编译器在翻译Task.Run时发生了什么?

谢谢


我进入了7层深度,然后VS2013就无响应了。可能是我的某个扩展(特别是Resharper)有问题;我没有检查到那一步。 - Igby Largeman
1个回答

6
什么原因导致编译器将L3与L1和L2区别对待?为什么仅仅在L1中添加'async'(或从L2中删除'await')就会导致编译器对其进行不同的处理?
因为asyncawait会改变lambda表达式中的类型。您可以将async视为“添加”一个Task<>包装器,将await视为“移除”Task<>包装器。
只需考虑最内层调用涉及的类型即可。首先是L1:
return Task.Run(() =>
{
  return new Dummy();
});
() => { return new Dummy(); }的类型为Func<Dummy>,因此Task.Run的该重载的返回类型为Task<Dummy>
因此,() => ###Task<Dummy>###的类型为Func<Task<Dummy>>,调用Task.Run的另一个重载,返回类型为Task<Dummy>。依此类推。
现在考虑L2:
return await Task.Run(async () =>
{
  return new Dummy();
});
async () => { return new Dummy(); }的类型是Func<Task<Dummy>>,因此该重载的Task.Run 的返回类型是Task<Dummy>async () => await ###Task<Dummy>###的类型也是Func<Task<Dummy>>,因此它调用了具有结果类型为Task<Dummy>相同重载的Task.Run。以此类推。
最后是L3:
return Task.Run(async () =>
{
  return new Dummy();
});
async () => { return new Dummy(); }的类型是Func<Task<Dummy>>,所以Task.Run重载的返回类型是Task<Dummy>async () => { return ###Task<Dummy>### }的类型是Func<Task<Task<Dummy>>>。请注意嵌套任务。因此,再次调用Task.Run相同的重载,但这次它的返回类型是Task<Task<Dummy>>
现在,您只需针对每个级别重复即可。 async () => { return ###Task<Task<Dummy>>### }的类型是Func<Task<Task<Task<Dummy>>>>。再次调用Task.Run相同的重载,但是这次它的返回类型是Task<Task<Task<Dummy>>>。等等。
如果将更多的Task.Run级联到L1 / L2 / L3,则Visual Studio会崩溃。我正在使用VS2013,如果级联5层或更多层的Task.Run,则会崩溃。4层是我能得到的最好结果,这就是为什么我将4层用作示例。只有我这样吗?编译器在转换Task.Run时发生了什么?
谁在乎呢?没有真实的代码会这样做。对于编译器来说,在合理的时间范围内处理极其���难的已知情况之一是在方法重载解析中使用lambda表达式。使用lambda表达式的嵌套调用使编译器工作成倍增加

太棒了,回答得非常好!现在我不再有任何疑虑了,谢谢。 - Jack Lee

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