如何等待异步方法完成?

199
我正在编写一个WinForms应用程序,用于向USB HID类设备传输数据。我的应用程序使用优秀的通用HID库v6.0,可以在这里找到。简而言之,当我需要向设备写入数据时,会调用以下代码:
private async void RequestToSendOutputReport(List<byte[]> byteArrays)
{
    foreach (byte[] b in byteArrays)
    {
        while (condition)
        {
            // we'll typically execute this code many times until the condition is no longer met
            Task t = SendOutputReportViaInterruptTransfer();
            await t;
        }

        // read some data from device; we need to wait for this to return
        RequestToGetInputReport();
    }
}

当我的代码跳出 while 循环后,我需要从设备中读取一些数据。但是,设备无法立即响应,因此在继续之前我需要等待此调用返回。目前,RequestToGetInputReport() 的声明如下:

private async void RequestToGetInputReport()
{
    // lots of code prior to this
    int bytesRead = await GetInputReportViaInterruptTransfer();
}

就算价值不大,GetInputReportViaInterruptTransfer() 的声明看起来像这样:

internal async Task<int> GetInputReportViaInterruptTransfer()

很遗憾,我对.NET 4.5中的新异步/等待技术不太熟悉。之前我读了一些await关键字的内容,这让我觉得在RequestToGetInputReport()内部调用GetInputReportViaInterruptTransfer()函数会等待(也许是这样?),但似乎RequestToGetInputReport()函数本身并没有等待,因为我几乎立即重新进入while循环。有人能澄清我看到的行为吗?
7个回答

303
关于asyncawait最重要的一点是,await并不会等待相关调用完成。如果操作已经完成,await会立即同步地返回操作结果;如果操作尚未完成,则会安排一个继续执行async方法余下部分的任务,并将控制权返回给调用者。当异步操作完成时,预定的完成任务将被执行。
对于您问题标题中的具体问题,答案是通过调用适当的Wait方法来阻塞async方法的返回值(应该是TaskTask<T>类型)。
public static async Task<Foo> GetFooAsync()
{
    // Start asynchronous operation(s) and return associated task.
    ...
}

public static Foo CallGetFooAsyncAndWaitOnResult()
{
    var task = GetFooAsync();
    task.Wait(); // Blocks current thread until GetFooAsync task completes
                 // For pedagogical use only: in general, don't do this!
    var result = task.Result;
    return result;
}

在这段代码片段中,CallGetFooAsyncAndWaitOnResult是异步方法GetFooAsync同步包装器。然而,大多数情况下应避免使用此模式,因为它会在整个线程池线程上阻塞异步操作的持续时间。这是对API公开的各种异步机制的低效利用,这些API花费了很大的精力来提供它们。
"await" doesn't wait for the completion of call的答案中,有几个更详细的解释这些关键字的解释。

同时,@Stephen Cleary对于async void的指导仍然适用。其他关于为什么不应该使用void异步方法的好的解释可以在http://www.tonicodes.net/blog/why-you-should-almost-never-write-void-asynchronous-methods/https://jaylee.org/archive/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void.html中找到。


26
我认为将await称为“异步等待”(asynchronous wait)是有帮助的,因为它可以阻塞方法(如果必要),但不会阻塞线程。因此,即使它不是阻塞等待,我们也可以说RequestToSendOutputReport在“等待”RequestToGetInputReport - Stephen Cleary
@Richard Cook - 非常感谢您的额外说明! - bmt22033
12
应该采纳这个答案,因为它更清楚地回答了实际问题(即如何对异步方法进行线程阻塞)。 - csvan
1
最佳解决方案是异步等待任务完成。var result = Task.Run(async () => { return await yourMethod(); }).Result; - Ram ch
@StephenCleary 为什么 C# 团队提供了 Result 属性和 Wait() 方法,如果它们因死锁而不能使用呢? - David Klempfner
4
@DavidKlempfner:在await出现之前,WaitResult已经存在于Task类型中。在await到来之前,Task是任务并行库的一部分,主要用于并行编程。 - Stephen Cleary

162
避免使用 async void。让你的方法返回 Task 而不是 void。这样你就可以 await 它们了。
像这样:
private async Task RequestToSendOutputReport(List<byte[]> byteArrays)
{
    foreach (byte[] b in byteArrays)
    {
        while (condition)
        {
            // we'll typically execute this code many times until the condition is no longer met
            Task t = SendOutputReportViaInterruptTransfer();
            await t;
        }

        // read some data from device; we need to wait for this to return
        await RequestToGetInputReport();
    }
}

private async Task RequestToGetInputReport()
{
    // lots of code prior to this
    int bytesRead = await GetInputReportViaInterruptTransfer();
}

1
非常好,谢谢。我也遇到了类似的问题,而解决方法就是像你说的那样将void改为Task - Jeremy
10
这只是小事,但为了遵循惯例,两种方法的名称都应该添加Async,例如RequestToGetInputReportAsync()。 - tymtam
8
如果呼叫者是主函数,那该怎么办? - symbiont
21
使用GetAwaiter().GetResult() - Stephen Cleary
如果知道返回任务而不是空值时所做的魔法,那将会非常棒。 - Ahmed Salah
5
Task代表方法的执行-因此,返回值放置在Task.Result上,异常放置在Task.Exception上。对于void方法,编译器没有地方存放异常,所以它们只是在线程池线程上重新引发。 - Stephen Cleary

134

等待异步方法完成任务的最佳解决方案是

var result = Task.Run(async() => await yourAsyncMethod()).Result;

30
或者对于您的异步“void”: Task.Run(async () => { await yourAsyncMethod(); }).Wait(); - Jiří Herník
1
这种方法相比于yourAsyncMethod().Result有什么好处? - Justin J Stark
5
仅访问.Result属性实际上并不等待任务执行完成。事实上,如果在任务完成之前调用它,我认为会抛出异常。 我认为使用Task.Run()包装这个方法的优点是,就像Richard Cook在下面提到的那样,"await"实际上并不等待任务完成,但使用.Wait()会阻塞整个线程池。这允许您在单独的线程上(同步)运行异步方法。有点令人困惑,但就是这样。 - Lucas Leblanc
如果答案能够提到a) 它在单独的线程池线程上运行yourAsyncMethod,b) 在尝试访问结果之前进行了Wait()操作,那么这将是一个很好的答案。如果您无法在当前线程中使用Wait()或"await",则线程池可能是一个合法的解决方案。 - tekHedd
1
@Lightning3 在异步方法中是否仍应该使用Task.Run()进行包装?还是直接使用.GetAwaiter().GetResult()而不使用包装器?这两种方法有什么区别?此外,为什么不使用Task.Run().Result? - String.Empty
显示剩余3条评论

8

只需将Wait()放置在代码中以等待任务完成

GetInputReportViaInterruptTransfer().Wait();


4
这会阻塞当前线程。因此,通常情况下这是一个不好的做法。 - Pure.Krome
24
有时候,这正是你所需要的。 - Michal Dobrodenka
有时候会导致应用程序阻塞,甚至死锁(特别是在使用调度程序/主线程的WPF中)。 - BerndK

0
以上所有答案都是正确的,你永远不应该同步等待任务……除非你必须这样做!有时候你想在一个你无法控制的接口实现中调用一个异步方法,而且没有办法“从上到下全部异步”。
这里是一个小类,至少可以避免一些损害:
public class RunSynchronous
{
    public static void Do(Func<Task> func) => Task.Run(func).GetAwaiter().GetResult();
    public static T Do<T>(Func<Task<T>> func) => Task.Run(func).GetAwaiter().GetResult();
    public static void Do(Func<ValueTask> func) => Do(() => func().AsTask());
    public static T Do<T>(Func<ValueTask<T>> func) => Do(() => func().AsTask());
}

这个类使用`.GetAwaiter.GetResult`模式来实际将异步转换为同步。但是,如果你在WPF或另一个绑定到特定线程的SynchronizationContext中单独执行此操作,会发生死锁。该代码通过将异步操作传输到未与特定线程同步的线程池来避免死锁。只要不要疯狂地阻塞所有线程池线程,你就应该没问题。
用法如下:
    return RunSynchronous.Do(()=>AsyncOperation(a,b,c));

将在线程池上启动AsynchronousOperation(a,b,c)并等待其返回。只要您不明确地同步回原始线程,您应该没问题。


-5
以下代码片段展示了一种确保等待的方法在返回给调用者之前完成的方式。然而,我不认为这是一个好的实践。如果您有不同的看法,请编辑我的答案并加以解释。
public async Task AnAsyncMethodThatCompletes()
{
    await SomeAsyncMethod();
    DoSomeMoreStuff();
    await Task.Factory.StartNew(() => { }); // <-- This line here, at the end
}

await AnAsyncMethodThatCompletes();
Console.WriteLine("AnAsyncMethodThatCompletes() completed.")

1
给那些点踩的人解释一下,就像我在回答中问的那样?因为据我所知,这个方法很有效... - Jerther
3
问题在于你只能通过将awaitConsole.WriteLine结合成一个Task来实现,这样就在两者之间放弃了控制权。因此,你的“解决方案”最终会产生一个Task<T>,这并没有解决问题。使用Task.Wait会导致实际上停止处理(可能发生死锁等问题)。换句话说,await实际上并不等待,它只是将两个可以异步执行的部分组合成一个单独的Task(某人可以查看或等待)。 - Ruben Bartelink

-7

实际上,我发现这对于返回IAsyncAction的函数更有帮助。

            var task = asyncFunction();
            while (task.Status == AsyncStatus.Completed) ;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接