简体   繁体   English

异步等待:线程是否一直运行到我等待?

[英]Async-await: Does the thread run until my await?

I always thought that if I call an async function, the thread starts executing this async function until it sees an await. 我一直以为,如果我调用异步函数,线程将开始执行此异步函数,直到看到等待状态为止。 Instead of waiting idly I thought that it would go up the call stack to see if the caller is not awaiting. 我没有闲着等待,我认为它会进入调用堆栈,以查看调用方是否在等待。 If not, it executes the code. 如果没有,它将执行代码。

Consider the following (simplified) code: 考虑以下(简化)代码:

async Task<string> FetchCustomerNameAsync(int customerId)
{
    // check if customerId is positive:
    if (customerId <= 0) throw new ArgumentOutofRangeException(nameof(customerId);

    // fetch the Customer and return the name:
    Customer customer = await FetchCustomerAsync(customerId);
    return customer.Name;
}

Now what happens if my async function would call FetchCustomerNameAsync(+1) without awaiting: 现在,如果我的异步函数不等待就调用FetchCustomerNameAsync(+1)会发生什么:

var myTask = FetchCustmerNameAsync(+1);
DoSomethingElse();
string customerName = await myTask;
  • FetchCustomerNameAsync , is called with a parameter value of +1 FetchCustomerNameAsync ,参数值为+1
  • FetchCustomerNameAsync detects that customerId is positive, so no exception FetchCustomerNameAsync检测到customerId为正,因此也不例外
  • FetchCustomerNameAsync calls FetchCustomerAsync FetchCustomerNameAsync调用FetchCustomerAsync
  • Somewhere inside FetchCustomerAsync is an await. FetchCustomerAsync内部的某个地方正在等待。 when that happens the thread goes up the call stack until one of the callers is not waiting. 当发生这种情况时,线程将进入调用堆栈,直到一个调用者不等待。
  • FetchCustomerNameAsync is awaiting, so up the call stack FetchCustomerNameAsync正在等待,因此调用堆栈上升
  • my function is not awaiting yet, continue with DoSomethingElse() 我的函数尚未等待,请继续执行DoSomethingElse()
  • my function meets the await. 我的功能正等待着您。

What I thought, is that before the await in my function is met, the check on the parameter value already has been done. 我的想法是,在满足我的功能中的等待之前,已经完成了对参数值的检查。

Therefore the following should lead to an exception before the await: 因此,以下内容应导致在等待之前发生异常:

// call with invalid parameter; do not await
var myTask = FetchCustmerNameAsync(-1);      // <-- note the minus 1!
Debug.Assert(false, "Exception expected");

I'd think that although I was not awaiting, that the check on parameter value was performed before the Debug.Assert . 我认为尽管我没有等待,但是参数值的检查是在Debug.Assert之前执行的。

Yet, in my program no exception is thrown before the Debug.Assert Why? 但是,在我的程序中,Debug.Assert为什么没有抛出异常 What is really happening? 到底发生了什么事?

Addition after comments 评论后加法

Apparently some people didn't want the simplified code, but my original test code. 显然有些人不想要简化的代码,而是我的原始测试代码。 Although I don't think it will help describing the problem, here it is. 尽管我认为这对描述问题没有帮助,但是确实如此。

Microsoft about usage of local functions in C# 7 . Microsoft有关在C#7中使用局部功能的信息
This article describes that an exception will not be detected before the await (as my question was). 本文介绍了在等待之前将不会检测到异常(因为我的问题是)。 This astounded me, because I always thought that the parameter was already checked. 这让我感到震惊,因为我一直认为该参数已经被检查。 So I wrote some test code. 所以我写了一些测试代码。 (now I know better, thanks to answerers and commentors). (现在,我已经知道了,多亏了回答者和评论者)。

So here is my Non-simplified test code. 这是我的非简化测试代码。 It compiles, it runs, and it shows the effect. 它编译,运行并显示效果。 However it does not help describing the question, it only distracts from it. 但是,这无助于描述问题,只会分散注意力。 But for those who are still interested after all these warnings: 但是对于那些仍然对所有这些警告仍然感兴趣的人:

async Task DemoLocalFunctionsInAwaitAsync()
    {
        // using local functions after parameterchecks gives errors immediately

        // No exception before await:
        Task<int> task1 = OldMethodWithoutLocalFunction(null);
        // See? no exception

        // New method: exception even if no await
        try
        {
            Task<int> task2 = NewMethodUsingLocalFunction(null);
            // no await, yet an exception
            Debug.Assert(false, "expected exception");
        }
        catch (ArgumentNullException)
        {
            // this exception is expected
        }

        try
        {
            // await the first task that did not throw an exception: expect the exception
            await task1;
            Debug.Assert(false, "expected exception");
        }
        catch (ArgumentNullException)
        {
            // this exception is expected
        }
    }

Below the function as I would normally write it: 在函数下面,就像我通常写的那样:

    async Task<int> OldMethodWithoutLocalFunction(Customer c)
    {
        // this does not throw exception before awaited
        if (c == null) throw new ArgumentNullException(nameof(c));
        await Task.CompletedTask;
        return c.CustomerId;
    }

This is the function that uses the local function. 这是使用局部函数的函数。 Almost as described in the Microsoft article mentioned above. 几乎如上所述的Microsoft文章中所述。

    async Task<int> NewMethodUsingLocalFunction(Customer c)
    {
        // this method gives an exception even if not awaited yet
        if (c == null) throw new ArgumentNullException(nameof(c));
        return await LocalFetchCustomerIdAsync(c);

        async Task<int> LocalFetchCustomerIdAsync(Customer customer)
        {
            await Task.CompletedTask;
            return customer.CustomerId;
        }
    }

If you look closely: this will also not help (and I understand now why, thanks to answerers and commentors). 如果您仔细观察:这也无济于事(感谢答复者和评论者,我现在明白为什么了)。

Exceptions only propagates when a task is awaited 异常仅在等待任务时传播

You can't handle an exception without awaiting the Task. 您必须等待任务才能处理异常。 The Exception(s) only propagate within a Thread/Task. 异常仅在线程/任务内传播。 So if you don't await, the Exception just stops the Task. 因此,如果您不等待,异常将停止任务。 And if the exception is thrown before you await, it will propagate when you actually await. 如果异常在您等待之前被抛出,它将在您实际等待时传播。

Do all the validations before, and then do the asynchronous work. 在进行所有验证之前,然后进行异步工作。

So, I suggest you to validate before: 因此,我建议您先进行验证:

ValidateId(id); // This will throw synchronously.
Task<Customer> customer = FetchCustomerAsync(id).ConfigureAwait(false);
DoSomethingElse();
return await customer.Name;

This is the best way to achieve the parallelism you want. 这是实现所需并行性的最佳方法。

You are right about that thread executes async function until it sees an await. 您对那个线程执行异步功能直到看到等待是正确的。 In fact your ArgumentOutofRangeException is thrown by the thread on which you call FetchCustmerNameAsync . 实际上,调用FetchCustmerNameAsync的线程会抛出ArgumentOutofRangeException The reason why you don't get exception even though it is the same thread is because when you use await inside a function, a AsyncStateMachine is built. 即使在同一个线程中也不会得到异常的原因是,当您在函数中使用await时,会生成AsyncStateMachine It converts all code into state machine, but the important part is how it handles exception. 它将所有代码转换为状态机,但是重要的部分是它如何处理异常。 Take a look: 看一看:

This code: 这段代码:

public void M() {

    var t = DoWork(1);

}

public async Task DoWork(int amount)
{
    if(amount == 1)
        throw new ArgumentException();

    await Task.Delay(1);
}

Gets converted into (I skipped unimportant parts): 转换成(我跳过了不重要的部分):

private void MoveNext()
{
    int num = <>1__state;
    try
    {
        TaskAwaiter awaiter;
        if (num != 0)
        {
            if (amount == 1)
            {
                throw new ArgumentException();
            }
            awaiter = Task.Delay(1).GetAwaiter();
            if (!awaiter.IsCompleted)
            {
                // Unimportant
            }
        }
        else
        {
            // Unimportant
        }
    }
    catch (Exception exception)
    {
        <>1__state = -2;
        <>t__builder.SetException(exception); // Add exception to the task.
        return;
    }
    <>1__state = -2;
    <>t__builder.SetResult();
}

If you follow <>t__builder.SetException(exception); 如果您遵循<>t__builder.SetException(exception); ( AsyncMethodBuilder.SetException ), you will find that it eventually calls task.TrySetException(exception); AsyncMethodBuilder.SetException ),您将发现它最终调用task.TrySetException(exception); which adds the exception to the task's exceptionHolder , which can be retrieved with Task.Exception property. 它将异常添加到任务的exceptionHolder ,可以使用Task.Exception属性进行检索。

A simplified MCVE : 简化的MCVE:

    static async Task Main(string[] args)
    {       
        try
        {
          // enable 1 of these calls
            var task = DoSomethingAsync();
          //  var task = DoSomethingTask();

            Console.WriteLine("Still Ok");
            await task;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);                
        }
    }

    private static async Task DoSomethingAsync()
    {
        throw new NotImplementedException();            
    }

    private static Task DoSomethingTask()
    {
        throw new NotImplementedException();
        return Task.CompletedTask;
    }

When you call DoSomethingAsync, you will see the "Still Ok" message. 当您调用DoSomethingAsync时,您将看到“ Still Ok”消息。

When you call DoSomethingTask, you will get the behaviour you expect: an immediate exception before the WriteLine. 调用DoSomethingTask时,您将获得预期的行为:WriteLine之前的立即异常。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM