C# 异步等待:线程是否运行到我的等待?

C# 异步等待:线程是否运行到我的等待?,c#,async-await,C#,Async Await,我一直认为,如果我调用一个异步函数,线程就会开始执行这个异步函数,直到它看到一个等待。我认为它将进入调用堆栈,查看调用方是否在等待,而不是无所事事地等待。如果不是,则执行代码 考虑以下(简化)代码: FetchCustomerNameAsync,调用时参数值为+1 FetchCustomerNameAsync检测到customerId为正,因此没有异常 FetchCustomerNameAsync调用FetchCustomerAsync FetchCustomerAsync中的某个地方是等待。

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

考虑以下(简化)代码:

  • FetchCustomerNameAsync
    ,调用时参数值为+1
  • FetchCustomerNameAsync
    检测到
    customerId
    为正,因此没有异常
  • FetchCustomerNameAsync
    调用
    FetchCustomerAsync
  • FetchCustomerAsync
    中的某个地方是等待。当这种情况发生时,线程将上升到调用堆栈,直到其中一个调用方不再等待
  • FetchCustomerNameAsync
    正在等待,因此请启动调用堆栈
  • 我的函数尚未等待,请继续执行
    DoSomethingElse()
  • 我的功能符合等待
我的想法是,在函数中的wait得到满足之前,已经完成了对参数值的检查

因此,在等待之前,以下情况应导致异常:

// call with invalid parameter; do not await
var myTask = FetchCustmerNameAsync(-1);      // <-- note the minus 1!
Debug.Assert(false, "Exception 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;
    }
异步任务OldMethodWithoutLocalFunction(客户c)
{
//这不会在等待之前引发异常
如果(c==null)抛出新的ArgumentNullException(nameof(c));
等待任务。完成任务;
返回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;
        }
    }
async Task NewMethodUsingLocalFunction(客户c)
{
//即使尚未等待,此方法也会给出异常
如果(c==null)抛出新的ArgumentNullException(nameof(c));
返回等待LocalFetchCustomerIdAsync(c);
异步任务LocalFetchCustomerIdAsync(客户)
{
等待任务。完成任务;
返回customer.CustomerId;
}
}
如果你仔细观察:这也不会有什么帮助(感谢回答者和评论者,我现在明白了原因)。

简化的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之前立即出现异常

简化的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之前立即出现异常

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

执行之前的所有验证,然后执行异步工作。 因此,我建议您在验证之前:

ValidateId(id); // This will throw synchronously.
Task<Customer> customer = FetchCustomerAsync(id).ConfigureAwait(false);
DoSomethingElse();
return await customer.Name;
ValidateId(id);//这将同步抛出。
任务客户=FetchCustomerSync(id).ConfigureAwait(false);
DoSomethingElse();
返回等待客户。名称;
这是实现所需并行性的最佳方法。

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

执行之前的所有验证,然后执行异步工作。 因此,我建议您在验证之前:

ValidateId(id); // This will throw synchronously.
Task<Customer> customer = FetchCustomerAsync(id).ConfigureAwait(false);
DoSomethingElse();
return await customer.Name;
ValidateId(id);//这将同步抛出。
任务客户=FetchCustomerSync(id).ConfigureAwait(false);
DoSomethingElse();
返回等待客户。名称;

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

关于线程执行异步函数直到看到等待为止的说法是正确的。事实上,
ArgumentOutofRangeException
是由调用
FetchCustNameAsync
的线程引发的。即使是同一个线程,您也不会得到异常的原因是,当您在函数中使用
wait
时,会生成一个
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___状态;
尝试
{
任务等待者;
如果(num!=0)
{
如果(金额=1)
{
抛出新ArgumentException();
}
waiter=Task.Delay(1.getwaiter();
如果(!waiter.IsCompleted)
{
//无关紧要
}
}
其他的
{
//无关紧要
}
}
捕获(异常)
{
1_uu状态=-2;
t__builder.SetException(exception);//将异常添加到任务中。
返回;
}
1_uu状态=-2;
t__builder.SetResult();
}

如果您遵循
t\uu builder.SetException(异常)
asynchmethodbuilder.SetException
),您会发现它最终调用了
task.TrySetException(exception)
将异常添加到任务的
异常持有者
,可以使用
task.exception
属性检索该异常。

线程执行异步函数直到看到等待。事实上,
ArgumentOutofRangeException
是由调用
FetchCustNameAsync
的线程引发的。即使是同一个线程,您也不会得到异常的原因是,当您在函数中使用
wait
时,会生成一个
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();
}