Skip to content

Latest commit

 

History

History
207 lines (136 loc) · 15.2 KB

8.md

File metadata and controls

207 lines (136 loc) · 15.2 KB

八、让代码异步

在版本 5 中,C# 引入了异步编写和调用代码的能力,通常称为异步。为了理解异步,考虑代码的正常行为是有用的,它是同步的。在同步代码中,您调用一个方法,等待它完成,然后继续执行代码的其余部分。这种行为的要点是调用同步方法的线程也在执行该方法中的同步代码。如果同步方法运行很长时间,您的用户界面将变得没有响应,您的用户可能不知道程序是否崩溃,或者他们是否应该等待。

异步代码通过允许长时间运行的操作在单独的线程上继续,并释放您的调用线程来恢复其职责,从而改善了这种情况。当调用线程是用户界面线程时,应用程序再次响应,您可以显示状态或繁忙指示器,或者让用户在异步进程运行时操作程序的另一部分。当异步过程返回时,您可以以某种方式与用户交互,如果这对您的应用程序有意义的话。过去,编写这种异步代码是一个挑战。尽管编写异步代码的任务随着新的模式和库而得到了改进,但是 C# 异步特性使得异步编程变得更加容易。

异步有两种不同的观点决定了你如何编码:库使用者还是库创建者。从消费者的角度来看,您基于隐含的契约对异步代码进行假设。然而,从库创建者的角度来看,您有额外的责任来确保您的代码提供用户期望的异步契约。

使用异步代码

C# 有两个支持异步的关键字:asyncawait。用async修饰符修饰方法表示该方法可以包含异步代码。您可以在Task上使用await关键字来启动异步操作。

    using System.IO;
    using System.Threading.Tasks;

    public class Program
    {
        public static void Main()
        {
            Program.CreateFileAsync("test.txt").Wait();
        }

        public static async Task CreateFileAsync(string filename)
        {
            using (StreamWriter writer = File.CreateText(filename))
                await writer.WriteAsync("This is a test.");
        }
    }

代码清单 112

在之前的程序中,CreateFileAsync方法是异步的。你可以通过方法上的async修饰符来判断。您需要为System.IOSystem.Threading.Tasks命名空间添加using子句,分别用于写入文件和异步Task支持。File类是 FCL 的一部分,它的CreateText方法返回一个用于写入文件的StreamWriter

| | 注意:不需要用异步附加方法名,但这是一个常见的约定。 |

调用异步方法的正确方法是await调用它的TaskTask<T>WriteAsync方法返回Task,也就是说你可以await它。

using语句在其封闭块完成时关闭文件。在这种情况下,块只有一行,因此不需要大括号。

异步契约的一部分是期望您正在使用的库中的一些代码将在另一个线程上运行操作,释放您的线程用于其他操作;WriteAsync也是这么做的。因此,线程返回到调用这个异步方法的代码。但是这个程序中的调用者是Main方法,在从CreateFileAsync返回的Task上调用Wait()。这防止程序在运行异步操作的线程完成之前结束。

| | 警告:前面的例子是一个控制台应用程序,它没有底层基础结构(称为同步上下文)来管理正确的线程处理。因此,有必要等待()从 CreateFileAsync 返回的任务。在普通的用户界面应用程序中,您将有一个同步上下文,这意味着您不必担心程序结束,也不需要在异步方法上调用 Wait()。等待异步方法的首选方法是通过异步和等待,如 CreateFileAsync 方法所示。事实上,您永远不应该在异步方法上调用等待。这是因为当第二个线程处理完异步调用返回时,它会尝试将调用封送回到调用线程。如果调用线程处于同步等待状态(),该线程将被阻塞,从而阻止第二个线程执行该封送处理操作。那你就会陷入僵局。为了防止死锁,请不要调用 wait(),而是使用 async 和 Wait。 |

如果使用await,方法上需要async修改器。如果一个方法有async修饰符,但是没有await修饰符,C# 会给你一个编译器警告,让你知道这个方法会同步运行。

异步返回类型

有了async,你可以await任意一个可选择的类型。FCL 有TaskTask<T>,这两个是可以选择的,也是你在大多数情况下应该使用的。返回Task意味着该方法不返回值,这是您在前面的CreateFileAsync方法中看到的。

| | 提示:斯蒂芬·图布的博客文章“等待任何事情解释如何创建自定义的可识别类型,如果您认为这是改进代码的一种方法,那么这是一个很好的参考。可以在http://blogs . msdn . com/b/pfxteam/archive/2011/01/13/10115642 . aspx上阅读。 |

当您的方法返回值时,使用Task<T>。下面的清单显示了一个例子。

        public async Task<string> ReturnGreeting()
        {
            await Task.Delay(1000);
            return "Hello";
        }

代码清单 113

Task.Delay是一种让线程休眠几毫秒的方法,但是我将在更多的例子中使用它来简化代码,并作为通常添加异步代码的占位符。

前面的例子显示了Task<string>的返回类型。该方法只返回字符串"Hello",而不是Task<string>的实例,因为 C# 编译器会为您处理这个问题。

异步方法可以返回void而不是可调用的类型。这在下面的列表中完成。

        public async void SayGreeting()
        {
            await Task.Delay(1000);
            Console.WriteLine("Hello");
        }

代码清单 114

这个方法是异步执行的,但是异步void方法有一些重要的警告,你必须注意:它们不是可感知的,也不能防止异常,但是对于像事件处理这样方法必须是void的情况,它们是必要的。

因为你只能等待像TaskTask<T>这样的可调用类型,所以你不可能等待异步void方法。这意味着当一个库的代码启动另一个线程时,它允许调用线程返回。调用异步void方法意味着您不能等到该方法完成,并且您永远不知道该方法何时或是否完成。就像任何事情一样,没有绝对的东西,人们可能会认为编写一些跨线程通信机制是可能的,但我指的是一般的开箱即用行为,这将导致一些重要的含义。由于这种行为,在什么时候应该使用异步void方法上有利弊。

async void方法最大的问题是不能将异常抛出回调用代码。通过TaskTask<T>返回方法,您可以await并将异步方法调用包装在try - catch中,但是您不能通过异步void方法来做到这一点。如果异步void方法引发未处理的异常,应用程序将崩溃。

有了这样的问题,很容易假设根本不应该使用异步void。然而,C# 语言设计者添加异步void有一个特定的原因:支持事件处理。中的事件处理程序。NET 框架遵循一种模式,即他们的委托返回void。因此,不能使用可调用的类型,如TaskTask<T>,必须将异步void方法指定为事件处理程序。

在用户界面应用程序中,用户界面控件可能会触发一个事件,分配给该事件的异步void方法执行,异步代码启动一个新线程并释放用户界面线程,用户界面线程返回并处理消息以保持用户界面的响应。因此,使用异步void作为事件处理程序是合适的。

开发异步库

编写异步库通常是正常的编码,但是要记住的关键是线程发生了什么。首先,默认情况下,所有代码都在调用线程上执行。其次,您需要将执行封送到一个新的线程上,并将调用线程释放给调用者。

了解代码在哪个线程上运行

下面的代码不一定有任何逻辑意义,但是代表了您可能编写的一些库代码的潜在结构。特别是,它演示了在您的async方法中的第一个await之前和之后线程会发生什么。在下面的代码中,UserInfo只是一个保存和返回数据的类型。UserServiceAddressServiceGetUserInfoAsync方法调用的异步方法。

    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;

    public class UserInfo
    {
        public string Info { get; set; }
        public string Address { get; set; }
    }

    class UserService
    {
        internal static async Task<string> GetUserAsync(string user)
        {
            // Do some long running synchronous processing.
            return await Task.FromResult(user);
        }
    }

    class AddressService
    {
        internal static async Task<string> GetAddressAsync(string user)
        {
            return await Task.FromResult(user);
        }
    }

    public class UserSearch
    {
        public async Task<UserInfo> GetUserInfoAsync(string term, List<string> names)
        {
            var userName =
                (from name in names
                 where name.StartsWith(term)
                 select name)
                .FirstOrDefault();

            var user = new UserInfo();
            user.Info = await UserService.GetUserAsync(userName);
            user.Address = await AddressService.GetAddressAsync(userName);

            return user;
        }
    }

代码清单 115

请记住,您正在编写可重用的库代码,因此可以从许多不同的技术调用它,例如 WPF、Windows Store 应用程序、Windows Phone 等等。这类应用程序的共同点是用户与用户界面控件交互,这些用户界面控件触发事件。这意味着异步void事件处理程序等待您的GetUserInfoAsync方法。

当事件处理程序代码调用您的代码时,它正在用户界面线程上运行。您的代码将继续在用户界面线程上运行,直到其他一些代码显式封送对另一个线程的调用并释放用户界面线程。

| | 注意:更准确地说,如果有另一个异步方法调用了您的代码并且已经释放了 UI 线程,那么调用您的代码的线程可能不一定是 UI 线程。然而,防御性编码是一种安全的方法,因为将来可能会有一些开发人员在 UI 线程上调用您的代码。 |

在到达第一个await之前,请注意GetUserInfoAsync中的 LINQ 查询。这是在调用线程上运行的同步代码,该线程也可以是用户界面线程。这里的问题是,用户界面线程被束缚在异步方法中工作,而不是返回到用户界面。想象一下,一个带有进度指示器的用户界面被锁定了,因为你的异步方法在第一次异步调用之前就已经抓住了用户界面线程并做了大量的处理。

调用UserService.GetUserAsync时代码还在 UI 线程上。我在GetUserAsync中添加了一个注释,以表示也在 UI 线程上运行的更长时间的同步处理。最后,等待Task.FromResult释放 UI 线程,剩下的代码异步运行。那是因为Task.FromResult正确地实现了异步契约。在向您展示如何修复这个问题之前,让我们看看代码的其余部分,这样您就可以理解它是如何运行的。

当代码从Task.FromResult返回时,UI 线程已经被释放,代码正在新的异步线程上运行。当从GetUserAsync返回到其调用者GetUserInfoAsync时,调用会自动封送回到调用线程,该线程可能是用户界面线程。同样,该程序会消耗用户界面线程上的 CPU 周期,从而降低应用程序的响应速度。幸运的是,有一种方法可以解决这个问题。

履行异步合同

上一节解释了默认情况下代码如何在调用线程上运行,该线程可能是用户界面线程。每当您在 FCL 调用异步方法时,该代码将释放调用线程并在新线程上继续,这是开发人员期望的异步契约的正确行为。您应该在代码中做同样的事情。

为此,使用Task.ConfigureAwait方法,传递false作为参数。下面是一个在GetUserInfoAsync解决问题的例子。

        public async Task<UserInfo> GetUserInfoAsync(string term, List<string> names)
        {
            var userName =
                (from name in names
                 where name.StartsWith(term)
                 select name)
                .FirstOrDefault();

            var user = new UserInfo();
            user.Info = await UserService.GetUserAsync(userName).ConfigureAwait(false);
            user.Address = await AddressService.GetAddressAsync(userName);

            return user;
        }

代码清单 116

GetUserInfoAsync方法将ConfigureAwait(false)附加到对GetUserAsync的调用中。GetUserAsync返回一个Task<string>ConfigureAwait(false)对该返回值进行操作,释放调用线程并在新的异步线程上运行该方法的剩余部分。就是这样;这就是你要做的。

在第一次调用ConfigureAwait之前,你还有同步处理的问题。有时候,你对此无能为力,因为有必要在第一个await之前执行该代码。但是,如果有可能在第一个await之后重新排列代码进行任何处理,您应该这样做。

再谈异步

我以前说过这一点,但我觉得值得重复。尤其是对于库代码,您应该更喜欢返回TaskTask<T>的异步方法。在用户界面中,如果你正在编写一个事件处理程序,你没有选择。如果您使用的是模型视图视图模型(MVVM)架构,您还需要void Command处理程序。在可重用库代码中不应该有这些问题,在这种情况下,异步void方法是危险的。

引发异常的异步 void 方法会使应用程序崩溃。

本章的大部分讨论都是围绕 async 如何通过释放 UI 线程来改善用户体验。除此之外,async 还通过不阻塞线程来提高应用程序性能。这些场景通常涉及某种类型的进程外操作,如网络通信、文件输入/输出或 REST 服务调用。这些操作可以在长时间运行的进程外操作执行时,使用 I/O 完成端口等 Windows 操作系统服务来释放线程;然后,当操作完成并需要返回到您的代码时,他们可以重新分配这些线程。除了通过高效的线程管理来提高性能之外,您还可以通过使用异步来提高服务器应用程序的可伸缩性,从而避免阻塞过多的线程。

尽管本章介绍了一些看似复杂的操作,但是通过使用异步,尝试执行许多与管理线程相关的操作来提高应用程序的响应能力、性能和可伸缩性变得更加容易。

总结

Async 是一种有用的功能,它允许您的应用程序响应迅速,性能良好。异步的用户体验是一个带有async修饰符的方法,并且能够等待异步方法的Task。除了用户体验之外,编写异步库还有其他注意事项。您应该知道线程行为,以及默认情况下async方法如何在调用方的线程上运行。请记住,您应该在第一个await之前最小化同步代码,并且应该尽早调用ConfigureAwait(false),释放用户界面线程并在新的异步线程上运行剩余的算法。