理解async和await存在问题

8

我一直在尝试理解C#中的async / await和Task,但是尽管观看了YouTube视频,阅读了文档并参加了Pluralsight课程,但都以惨败告终。

我希望有人能够回答这些稍微抽象的问题,以帮助我理解。

1. 为什么他们说使用async / await可以实现“异步”方法,而单独的async关键字什么也不做,而await关键字会添加一个暂停点?难道添加一个暂停点不是强制方法以同步方式工作,即在移动到下一个任务之前完成由await标记的任务吗?

2. 明显地,除了事件处理程序之外,您不应该使用async void,那么如何正常调用异步方法?似乎为了使用await关键字调用异步方法,调用它的方法/类本身需要被标记为async。我看过的所有示例都用一个事件处理程序“启动”了一个async void方法。如何“逃脱”这种包装async / await来运行方法呢?

3.

public async Task SaveScreenshot(string filename, IWebDriver driver)
{
    var screenshot = driver.TakeScreenshot();
    await Task.Run(() =>
    {
        Thread.Sleep(2000);
        screenshot.SaveAsFile(filename, ScreenshotImageFormat.Bmp);
        Console.WriteLine("Screenshot saved");
    });
    Console.WriteLine("End of method");
}

回到1,这看起来像是一个同步方法。当它到达Task.Run时,执行会暂停,因此Console.WriteLine("End of method");直到任务完成后才会被执行。也许整个方法本身在代码中触发时将异步执行?但是回到2,你需要使用await调用它,否则你会得到消息'Because this call is not awaited..',因此添加一个await将导致该执行点同步等等。

希望这能帮助你更好地理解。

3个回答

10

添加悬挂点不是会强制该方法同步执行,也就是在继续执行之前完成由 await 标记的任务吗?

不是的,你想到的词应该是“顺序”,而不是“同步”。await导致异步顺序代码。 “顺序”指“一个接一个地”; “同步”意味着“阻塞直到完成”。

通常如何调用异步方法?

使用 await

如何“逃脱”这个包装的 async/await 来运行该方法?

理想情况下,你不要这样做。你需要async all the way。现代框架(包括 ASP.NET MVC、Azure Functions/WebJobs、NUnit/xUnit/MSTest 等)都允许你有返回 Task 的入口点。较旧的框架(包括 WinForms、WPF、Xamarin Forms、ASP.NET WebForms 等)都允许 async void 入口点。

所以,理想情况下,您不应从同步代码调用异步代码。如果您考虑异步代码的本质,这是有道理的:它的全部意义在于不阻塞调用线程,因此如果您在异步代码上阻塞调用线程,则首先失去了异步代码的所有优势。
话虽如此,有时确实需要将代码视为同步处理。例如,如果您正在进行到异步转换的中间状态,或者如果您受限于强制使您的代码成为同步代码并且无法使用“async void”的库/框架。在这种情况下,您可以采用我在我的旧项目异步文章中提到的其中一种技巧。

谢谢Stephen。你说得对,我混淆了顺序和异步。这个解释是否有效 - 假设我有一个包含Task.Run的普通方法,并在其中添加一些代码。如果我触发该方法,在它到达Task.Run时,它将异步执行(?),但没有顺序控制,因此方法中后面的代码将继续执行。这个方法本身会阻塞调用线程吗?与async await相比,如果在方法中使用await前缀,则执行将等待任务运行,但不会阻塞调用线程。 - Konzy262
Task.Run只是将一些工作投放到线程池中。因此,我认为在Task.Run中的代码与调用代码完全独立。您可以将其视为相对于调用线程异步运行,但它与async/await使用的异步方式完全不同,因此我会使用术语“并行”来描述Task.Run而不是“异步”。您可能会发现我的异步介绍有所帮助。 - Stephen Cleary

4

您的理解非常好:)。您似乎忽略了一个主要点,即.NET中的“异步”方法是指可以在不阻塞调用线程的情况下停止执行的方法。

正如您在(1)中指出的那样,“async”关键字基本上启用了“await”的使用,并要求返回类型为“void”或“Task / Task<T>”。 “await”只是指示当前方法暂停执行,直到任务完成。

您在这里忽略的是它仅暂停当前方法。它不会阻塞方法正在执行的线程。在WPF应用程序的UI线程等情况下,这很重要。暂停方法执行并使一切保持运行状态,阻塞线程则会导致应用程序停止响应。

通常,您希望您的async调用一直到顶部(例如事件处理程序),这样可以提供最大的灵活性并防止死锁情况。但是,您可以使用Wait等待完成返回Task的方法:

someAsyncMethod.Wait()

或者获取返回值:

 var result = someAsyncMethod.Result;

请注意,这两种方法都是同步的,并会阻塞调用线程。如果异步任务正在等待调用线程上的其他工作完成,这样做可能会导致死锁。
以上内容应该回答了你在第三点中的问题;该方法本身似乎是同步执行的(这是await/async的魔力),但任务不会阻塞调用线程。

非常感谢Bradley。因此,举个例子,如果您调用异步方法并包含等待任务,该任务运行5分钟,当它达到等待时,“焦点”是否返回到调用线程?以一种“好的,我将控制权返回给您,让您自己执行任何操作,同时我执行此任务,之后我还会执行任何剩余的代码”类型的方式? - Konzy262
@Konzy262 是的,具体如何工作取决于上下文同步,就像Stephen的回答所解释的那样。 - BradleyDotNET

2

它是异步的,因为你不需要等待方法返回。在你的代码中,你可以调用异步方法并将任务保存在变量中。继续做其他事情。稍后,当需要方法结果时,你可以等待响应(任务)。

// Synchronous method.
static void Main(string[] args)
{
    // Call async methods, but don't await them until needed.
    Task<string> task1 = DoAsync();
    Task<string> task2 = DoAsync();
    Task<string> task3 = DoAsync();

    // Do other stuff.

    // Now, it is time to await the async methods to finish.
    Task.WaitAll(task1, task2, task3);

    // Do something with the results.
    Console.WriteLine(task1.Result);
    Console.ReadKey();
}

private static async Task<string> DoAsync()
{
    Console.WriteLine("Started");
    await Task.Delay(3000);
    Console.WriteLine("Finished");

    return "Success";
}

// Output:
// Started
// Started
// Started
// Finished
// Finished
// Finished
// Success

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