不等待异步调用仍然是异步的,对吗?

时间:2019-09-20 22:02:23

标签: c# .net asynchronous async-await

很抱歉,这是一个愚蠢的问题(或重复的问题)。

我有一个函数A

public async Task<int> A(/* some parameters */)
{
    var result = await SomeOtherFuncAsync(/* some other parameters */);

    return (result);
}

我还有另一个函数B,它调用A但不使用返回值:

public Task B(/* some parameters */)
{
    var taskA = A(/* parameters */); // #1

    return (taskA);
}

请注意,B没有声明为async,并且没有等待对A的调用。对A的呼叫不是一劳永逸的呼叫-B呼叫C就像这样:

public async Task C()
{
    await B(/* parameters */);
}

请注意,在#1 ,没有await。我有一个同事声称,这使得对A的调用是同步的,他不断提出Console.WriteLine日志,似乎证明了他的观点。

我试图指出,只是因为我们不等待B中的结果,所以等待了任务链 ,而A中的代码本质却没有不会因为我们不等待而改变。由于不需要A的返回值,因此只要链中有人等待它,就无需在调用站点等待任务。(在{{1}中发生}。

我的同事非常执着,我开始怀疑自己。我的理解错了吗?

2 个答案:

答案 0 :(得分:28)

  

很抱歉,这是一个愚蠢的问题

这不是一个愚蠢的问题。这是一个重要的问题。

  

我有一个同事声称这使对A的调用是同步的,并且他不断提出Console.WriteLine日志,这似乎证明了他的观点。

那是当前的基本问题,您需要教育您的同事,以免他们误导自己和他人。 没有异步调用之类的东西通话不是异步的,永远。跟我说在C#中调用不是异步的。在C#中,调用函数时,在计算完所有参数后立即调用该函数

如果您的同事或您认为存在异步调用之类的事情,那么您就痛苦不堪,因为您对异步工作方式的信念将与现实脱节。

那么,您的同事正确吗?当然是。 由于所有函数调用都是同步的,因此对A的调用是同步的。但是,他们相信存在“异步调用”之类的事实,这意味着他们对C#中异步的工作方式有严重的误解。

如果您的同事特别认为await M()以某种方式使M()的调用“异步”,那么您的同事会产生很大的误解。 await运算符。可以肯定,它是一个复杂的运算符,但它是一个运算符,并且对值进行运算。 await M()var t = M(); await t;是同一件事。等待发生在调用之后 ,因为await 对返回的值进行操作await不是的指令,指示编译器“生成对M()的异步调用”或任何此类事物;没有所谓的“异步调用”。

如果这是他们错误信念的本质,那么您就有机会教育您的同事await的含义。 await意味着简单但功能强大。这意味着:

  • 看看我正在使用的Task
  • 如果任务异常完成,则抛出该异常
  • 如果任务正常完成,请提取该值并使用它
  • 如果任务不完整,请将该方法的其余部分注册为等待的任务的继续,并返回表示此调用的 new Task 我的呼叫者的异步工作流程不完整。

这就是await的全部工作。它只是检查任务的内容,如果任务不完整,它会说:“好吧,在该任务完成之前,我们无法在此工作流程上取得任何进展,因此请返回给我的调用方,它将为该CPU找到其他内容要做”。

  

A中代码的性质不会因为我们不等待而改变。

是的。我们同步调用A,它返回一个Task。直到A返回,呼叫站点之后的代码才会运行。关于A的有趣之处在于,允许 A向其调用者返回不完整的Task ,并且该任务表示异步节点工作流程。工作流已经是 异步的,并且您注意到,它对A的返回值 返回后的操作没有影响。 A不知道您是否要await返回的TaskA会一直运行,然后返回正常完成的任务或异常完成的任务,或者返回不完整的任务。但是您在呼叫站点上所做的任何事情都无法改变这一点。

  

由于不需要A的返回值,因此无需在呼叫站点等待任务

正确。

  

只要链上的某人等待任务(在C中发生),就无需在呼叫站点等待任务。

现在你迷失了我。为什么有人必须等待Task返回的A说出为什么您认为某人需要{em> awaitTask,因为您可能有错误的信念。

  

我的同事非常执着,我开始怀疑自己。我的理解错了吗?

您的同事几乎可以肯定是错误的。您的分析似乎是正确的,直到您说有一个要求,要求每个Task都要await,这是不正确的。并非await一个Task奇怪的,因为这意味着您编写的程序是在其中开始操作的,并不关心何时或如何完成,当然闻起来很糟糕编写这样的程序,但是每个await都没有{em>要求。如果您再次相信存在,请说出该信念是什么,我们将对其进行整理。

答案 1 :(得分:6)

你是对的。创建任务只是这样做,它并不关心何时以及谁将等待其结果。尝试将await Task.Delay(veryBigNumber);放在SomeOtherFuncAsync中,控制台输出应该是您期望的。

这被称为淘汰,我建议您阅读this blogpost,在这里您可以了解为什么应该或不应该这样做。

还有一些最小的(有点麻烦的)示例复制了代码,证明您是正确的:

class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine($"Start of main {Thread.CurrentThread.ManagedThreadId}");
            var task = First();
            Console.WriteLine($"Middle of main {Thread.CurrentThread.ManagedThreadId}");
            await task;
            Console.WriteLine($"End of main {Thread.CurrentThread.ManagedThreadId}");
        }

        static Task First()
        {
            return SecondAsync();
        }

        static async Task SecondAsync()
        {
            await ThirdAsync();
        }

        static async Task ThirdAsync()
        {
            Console.WriteLine($"Start of third {Thread.CurrentThread.ManagedThreadId}");
            await Task.Delay(1000);
            Console.WriteLine($"End of third {Thread.CurrentThread.ManagedThreadId}");
        }
    }

这将Middle of main写在End of third之前,证明它实际上是异步的。此外,您可以(最有可能)看到函数的末端与程序的其余部分在不同的线程上运行。 main的开始和中间始终将在同一线程上运行,因为实际上它们是同步的(main开始,调用函数链,第三次返回(它可能在与await关键字一起返回),然后是main继续,就好像没有涉及异步函数一样。两个函数中await关键字之后的结尾可以在ThreadPool中的任何线程上运行(或在您使用的同步上下文中)。

现在值得注意的是,如果Task.Delay中的Third花费的时间并不很长,并且实际上是同步完成的,那么所有这些都将在单个线程上运行。而且,即使它可以异步运行,它也可能都在单个线程上运行。没有规则说明异步函数将使用多个线程,它很可能在等待一些I / O任务完成的同时做其他工作。