代码之家  ›  专栏  ›  技术社区  ›  Harald Coppoolse

异步等待:线程是否在等待前运行?

  •  0
  • Harald Coppoolse  · 技术社区  · 6 年前

    我一直认为,如果调用一个异步函数,线程就会开始执行这个异步函数,直到它看到等待。我不想无所事事地等待,而是想让它进入调用堆栈,看看调用方是否在等待。如果没有,它将执行代码。

    考虑以下(简化)代码:

    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调用
    • 获取客户名称同步 检测到 customerId 是肯定的,所以没有例外
    • 获取客户名称同步 FetchCustomerAsync
    • DoSomethingElse()

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

    Debug.Assert

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

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

    3 回复  |  直到 6 年前
        1
  •  1
  •   FCin    6 年前

    您对线程执行异步函数的看法是正确的,直到它看到等待。事实上你的 ArgumentOutofRangeException 被你调用的线程抛出 FetchCustmerNameAsync . 即使是同一个线程,也不会得到异常的原因是当您使用 await 函数内部,a 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 财产。

        2
  •  2
  •   Liam Joshua    6 年前

    只有在等待任务时才会传播异常

    如果不等待任务,就无法处理异常。异常仅在线程/任务中传播。因此,如果您不等待,异常只会停止任务。如果在等待之前抛出异常,它将在实际等待时传播。

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

    因此,我建议您在以下时间之前进行验证:

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

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

        3
  •  -1
  •   bommelding    6 年前

    简化的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时,您将看到“仍然正常”消息。

    当您调用dosomethingtask时,您将获得预期的行为:在writeline之前立即出现异常。