繁体   English   中英

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

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

我一直以为,如果我调用异步函数,线程将开始执行此异步函数,直到看到等待状态为止。 我没有闲着等待,我认为它会进入调用堆栈,以查看调用方是否在等待。 如果没有,它将执行代码。

考虑以下(简化)代码:

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;
}

现在,如果我的异步函数不等待就调用FetchCustomerNameAsync(+1)会发生什么:

var myTask = FetchCustmerNameAsync(+1);
DoSomethingElse();
string customerName = await myTask;
  • FetchCustomerNameAsync ,参数值为+1
  • FetchCustomerNameAsync检测到customerId为正,因此也不例外
  • FetchCustomerNameAsync调用FetchCustomerAsync
  • FetchCustomerAsync内部的某个地方正在等待。 当发生这种情况时,线程将进入调用堆栈,直到一个调用者不等待。
  • FetchCustomerNameAsync正在等待,因此调用堆栈上升
  • 我的函数尚未等待,请继续执行DoSomethingElse()
  • 我的功能正等待着您。

我的想法是,在满足我的功能中的等待之前,已经完成了对参数值的检查。

因此,以下内容应导致在等待之前发生异常:

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

我认为尽管我没有等待,但是参数值的检查是在Debug.Assert之前执行的。

但是,在我的程序中,Debug.Assert为什么没有抛出异常 到底发生了什么事?

评论后加法

显然有些人不想要简化的代码,而是我的原始测试代码。 尽管我认为这对描述问题没有帮助,但是确实如此。

Microsoft有关在C#7中使用局部功能的信息
本文介绍了在等待之前将不会检测到异常(因为我的问题是)。 这让我感到震惊,因为我一直认为该参数已经被检查。 所以我写了一些测试代码。 (现在,我已经知道了,多亏了回答者和评论者)。

这是我的非简化测试代码。 它编译,运行并显示效果。 但是,这无助于描述问题,只会分散注意力。 但是对于那些仍然对所有这些警告仍然感兴趣的人:

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
        }
    }

在函数下面,就像我通常写的那样:

    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;
    }

这是使用局部函数的函数。 几乎如上所述的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;
        }
    }

如果您仔细观察:这也无济于事(感谢答复者和评论者,我现在明白为什么了)。

异常仅在等待任务时传播

您必须等待任务才能处理异常。 异常仅在线程/任务内传播。 因此,如果您不等待,异常将停止任务。 如果异常在您等待之前被抛出,它将在您实际等待时传播。

在进行所有验证之前,然后进行异步工作。

因此,我建议您先进行验证:

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

这是实现所需并行性的最佳方法。

您对那个线程执行异步功能直到看到等待是正确的。 实际上,调用FetchCustmerNameAsync的线程会抛出ArgumentOutofRangeException 即使在同一个线程中也不会得到异常的原因是,当您在函数中使用await时,会生成AsyncStateMachine 它将所有代码转换为状态机,但是重要的部分是它如何处理异常。 看一看:

这段代码:

public void M() {

    var t = DoWork(1);

}

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

    await Task.Delay(1);
}

转换成(我跳过了不重要的部分):

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();
}

如果您遵循<>t__builder.SetException(exception); AsyncMethodBuilder.SetException ),您将发现它最终调用task.TrySetException(exception); 它将异常添加到任务的exceptionHolder ,可以使用Task.Exception属性进行检索。

简化的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;
    }

当您调用DoSomethingAsync时,您将看到“ Still Ok”消息。

调用DoSomethingTask时,您将获得预期的行为:WriteLine之前的立即异常。

暂无
暂无

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

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