等待Task.CompletedTask和返回的区别

40

我正在尝试理解await Task.CompletedTaskreturn之间的区别,但似乎找不到任何明确定义的解释。

为什么/何时会使用这个:

public async Task Test()
{
    await Task.CompletedTask;
}

关于这个问题?

public async Task Test()
{
    return;
}

据我所知,两个任务的status都将被设置为TaskStatus.RanToCompletion,虽然根据Task.FromResult的说明,这可能与物理时间或类似的东西有关:

当一个任务的返回值在不执行较长的代码路径的情况下立即知道时,通常会使用该方法。

我希望能够清楚地解释一下这一点,因为我已经研究了GitHub上的MS代码、MS文档以及我可以找到的所有链接,但是没有地方给出清晰的解释。我还看到在较大的方法末尾使用await Task.CompletedTask,根据我从MS on GitHub中找到的一些评论,这实际上是一个错误,因为它不应该包含在内,他们想要将其从存储库中清除。

如果还可以对Task.FromResult进行清晰的解释(因为它们是兄弟关系),那将不胜感激,因为我仍然不清楚何时使用:

public async Task<bool> Test()
{
    return await Task.FromResult(true);
}

超出这个限制:

public async Task<bool> Test()
{
    return true;
}

10
通常情况下,返回的是Task.CompletedTaskTask.FromResult(...),而不是等待它们。这样可以让您实现一个返回Task/Task<T>的方法,而无需使用async机制。 - Julien Couvreur
谢谢,这确实有帮助。所以当你使用async/await时,只需要返回/返回值而不是Task? - Storm
2个回答

37

让我们从消费者的角度来看这个问题。

如果您定义了一个接口,它强制执行返回一个Task的操作,那么您并没有说明它将如何被计算/执行(因此,在方法签名中没有async访问修饰符)。这是一个实现细节

接口

public interface ITest
{
    Task Test();
    Task<bool> IsTest();
}

因此,如何实现接口取决于您。

您可以同步方式进行操作,在没有async关键字的情况下不会生成任何AsyncStateMachine

实现 #1

public class TestImpl : ITest
{
    public Task Test()
    {
        return Task.CompletedTask;
    }

    public Task<bool> IsTest()
    {
        return Task.FromResult(true);
    }
}

或者您可以尝试以异步方式实现,但不使用await运算符。在这种情况下,您将收到CS1998警告。

实现 #2

public class TestImpl : ITest
{
    public async Task Test()
    {
        return;
    }

    public async Task<bool> IsTest()
    {
        return true;
    }
}

这个异步方法缺少'await'运算符,将同步运行。考虑使用'await'运算符等待非阻塞API调用,或者使用'await Task.Run(...)'在后台线程上执行CPU绑定的工作。
换句话说,此实现未定义状态机。异步方法基于await关键字分为不同的状态:
  • 在第一个await之前,
  • 在第一个await之后但在第二个await之前,
  • 在第二个await之后但在第三个await之前,
  • ...
如果没有任何await,则只有一个状态,该状态将同步运行(无需保留状态,执行异步操作,然后调用MoveNext())。

或者您可以尝试使用await运算符以异步方式实现。

实现方法 #3

public class TestImpl : ITest
{
    public async Task Test()
    {
        await Task.CompletedTask;
    }

    public async Task<bool> IsTest()
    {
        return await Task.FromResult(true);
    }
}

在这种情况下,将会有一个异步状态机,但它不会被分配在堆上。默认情况下,它是一个结构体,如果它以同步方式完成,则我们不需要在堆上分配它们以扩展其生命周期超出方法范围。
更多信息请阅读以下文章:
- .NET Core 3.x中的异步/等待 - .NET 5中的异步ValueTask池化 - 剖析C#中的异步方法

1
我得出结论,实现方案#3虽然避免了编译器警告,但效率较低,这个结论正确吗? - Moby Disk
1
@MobyDisk 是的,正确的。 - Peter Csala
1
@MobyDisk 让我纠正一下,只有第二个可能会导致编译时警告。 - Peter Csala
1
@Mike 基本上两者都会同步运行,所以唯一的区别就是生成的 AsyncStateMachine,它是一个结构体,因此会给 GC 增加更多压力。编译器/运行时可能足够聪明,可以摆脱 async/await 关键字... 但这是微观优化,所以我认为你不应该过于担心这个问题。 - Peter Csala
1
@Mike 我能看出第三个选项比第一个更有优势,尤其是当你知道很快就会用真正的异步方法调用来替换它时。 - Peter Csala
显示剩余2条评论

8

return; 表示你退出当前函数。

await Task.CompletedTask; 只是继续执行函数的其余部分。


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