【问题标题】:Async-await: Does the thread run until my await?Async-await:线程是否一直运行到我的等待?
【发布时间】:2018-07-31 17:39:38
【问题描述】:

我一直认为如果我调用一个异步函数,线程就会开始执行这个异步函数,直到它看到一个等待。我认为它会向上调用堆栈以查看调用者是否没有等待,而不是无所事事地等待。如果不是,则执行代码。

考虑以下(简化的)代码:

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 之前没有抛出异常 为什么?到底发生了什么?

在 cmets 之后添加

显然有些人不想要简化的代码,而是我的原始测试代码。虽然我认为这无助于描述问题,但就是这样。

Microsoft about usage of local functions in 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;
        }
    }

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

【问题讨论】:

  • 您的无效参数似乎是正数,因此不确定您为什么会期待异常。 (当然,这可能只是问题的转录错误,但这就是为什么我们更喜欢看到已执行的真实代码并且只是复制到问题中)跨度>
  • 因此,与其给我们提供不是复制和粘贴的“简化”代码,请给我们minimal reproducible example。您知道此问题中的代码 演示了您要询问的问题吗?
  • 您的异常已被存储并将在等待时抛出。你的调试器告诉你什么?
  • 异常存储在任务中。当您等待包含异常的任务时,会在该点引发异常。因为您不等待FetchCustmerNameAsync,所以异常只是坐在任务中。
  • 或者它命名了一个不存在的异常类型,语法不正确,并且显然没有放在编译器前面。这就是我问的原因。

标签: c# async-await


【解决方案1】:

异常仅在等待任务时传播

您无法在不等待任务的情况下处理异常。异常仅在线程/任务内传播。因此,如果您不等待,异常只会停止任务。如果在你等待之前抛出异常,它会在你真正等待时传播。

先做所有验证,再做异步工作。

所以,我建议你先验证一下:

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

这是实现所需并行度的最佳方式。

【讨论】:

  • 有道理(因为已经是 cmets 之一)。显然DoSomethingElse 会被执行,即使FetchCustomerAsync() 有异常。很公平,因为程序员表示FetchCustomerAsync的结果不会影响DoSomethingElse,否则他应该在DoSomethingElse之前等待
  • @HaraldCoppoolse 如果有正确答案,请标记。如果没有,请说明您的要求,以便我们为您提供帮助。
【解决方案2】:

您说得对,该线程执行异步函数,直到它看到等待。实际上,您的ArgumentOutofRangeException 是由您调用FetchCustmerNameAsync 的线程抛出的。即使它是同一个线程也不会出现异常的原因是因为当您在函数中使用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();
}

如果你关注&lt;&gt;t__builder.SetException(exception);AsyncMethodBuilder.SetException),你会发现它最终调用了task.TrySetException(exception);,将异常添加到任务的exceptionHolder,可以通过Task.Exception属性检索。

【讨论】:

    【解决方案3】:

    一个简化的 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 之前的立即异常。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-09-24
      • 2020-11-19
      • 1970-01-01
      • 1970-01-01
      • 2021-12-11
      • 2016-03-08
      • 1970-01-01
      相关资源
      最近更新 更多