(语法基础)浅谈C#中的 async await 以及对线程相关知识的复习
C#5.0以后新增了一个语法糖,那就是异步方法async await,之前对线程,进程方面的知识有过较为深入的学习,大概知道这个概念,我的项目中实际用到C#异步编程的场景比较少,就算要用到一般也感觉Task类也基本够用了,所以没有稍微仔细的去研究过这个语法,今天借工作闲暇来梳理一下这个知识点,顺便复习一下线程相关方面的知识,要搞懂这个知识点,需要有一定的基础知识,首先要知道,什么是线程,什么是同步,什么是异步
1. 线程?异步?同步?
什么是线程?规范定义说线程是程序执行流的最小单元,同一时间内只能做一件事情,请说人话,啥意思,举个栗子,假如把人比作一个多线程的软件(此处举例只是为了便于理解,实际中的人脑应该是多进程的),现在这个软件需要做一件叫做吃饭的任务,那么在同一时间内就开启了一个负责吃饭的线程,那这个线程就只能吃饭,不能做其他的事情,如果他想在吃饭的时候“边吃饭边看电视”,就要启用另一个线程,一个线程负责吃饭,一个线程负责看电视。在这里我为啥要把边吃饭边看电视打上引号?因为实际的多线程执行机制中,“边吃饭边看电视”这个两个任务并不是两个任务各在同一时间各自执行,而是快速且交替执行的,也就是两个任务在同一时间只能执行其中一个任务,用一张图来便于理解两个线程如何快速且交替执行,这种线程的执行方式叫做并发执行,而多线程同时执行只是系统带来的一个假像,它在多个单位时间内进行多个线程的切换。因为切换频密而且单位时间非常短暂,所以多线程可被视作同时运行。
下面看一段代码
static void Main(string[] args) { //创建 th1 th2两个线程,分别输出字母e和字母w,模拟执行吃饭和看电视两个任务 Thread th1 = new Thread(() => { for (int i = 0; i < 10000; i++) { Console.Write("e");//输出eat } }); Thread th2 = new Thread(() => { for (int i = 0; i < 10000; i++) { Console.Write("w");//输出watch } }); th1.Start();//启动th1线程 th2.Start();//启动th2线程 Console.ReadKey(); }
我们用for循环输出不同的字母来模拟把两个线程执行的任务碎片化最后看如下执行结果
可以看到输出结果字母e和w是不规则交替输出的,当然你可以使将for循环的i的最大设置的更大一些,来使执行效果更明显,如果你在两个线程内部打上断点,最后运行发现,两个线程在不断的交替执行。
能理解线程之间在同一时间内是并发执行的,那么也就不难理解什么是同步,什么是异步了,同步执行可以理解为代码一行一行按照顺序执行,即完成吃饭的任务后才可以执行看电视,所以一般情况下同步任务,一个线程就可以搞定,异步执行即两个任务的代码代码是交替执行,即吃饭和看电视两个任务是交替运行的,一个线程执行吃饭,一个线程执行看电视,所以不难理解一般涉及到同步异步的东西基本都会跟多线程挂钩。
异步编程的优点:合理的运用异步编程能极大的提升我们的代码运行效率,举一个简单的运用场景,比如界面加载数据渲染时,加载某个模块比较耗时,如果运用同步执行可能会出现加载耗时模块时界面卡死的情况,我们就可以把耗时的模块放在异步任务中,这样就不会影响其他模块的正常加载。ajax应该就是最常见的一个异步编程场景
异步编程是否一定就比同步编程好?
首先代码异步执行CPU需要花费不少的时间在线程的切换上,线程切换也有细微的性能损耗,所以过多地使用多线程反而会导致程序性能的下降。
而且不合理使用线程会造成线程冲突,下面代码我们分别用两个线程去对变量a进行十次++和五次--操作,然后多次运行这个程序,发现每次输出a的结果可能是不一样的,这就是两个线程代码执行顺序的不确定性导致每次执行算出的a的结果都不一样,也就是所谓的异步执行导致线程之间共享数据的冲突
static void Main(string[] args) { int a = 0; Thread th1 = new Thread(() => { for (int i = 0; i < 10; i++) { a++; } }); Thread th2 = new Thread(() => { for (int i = 0; i < 5; i++) { a--; } }); th1.Start(); th2.Start(); Console.Write(a); Console.ReadKey(); }
2. Thread vs Task
上面讲了这么多关于线程,同步,异步的东西,似乎不用 async await语法我们也能实现异步编程,那么 async await语法与传统的ThreadPool.QueueUserWorkItem启动线程(也就是上面的Thread)有什么区别呢? async await的好处又在哪呢?
上面的用代码传统的Thread方法启动线程虽然感觉很简单方便,但是我们我们仔细查看Thread类的构造函数的参数,发现Thread构造函数中所传递的委托是无参数,无返回值的,所以Thread所执行的异步函数的是没有参数,也没有返回结果的,这无疑是个很大的限制
而且对比Thread(线程)和async中Task(任务),二者之间有如下区别
1、任务是架构在线程之上的,也就是说任务最终还是要抛给线程去执行。
2、任务跟线程不是一对一的关系,比如开10个任务并不是说会开10个线程,这一点任务有点类似线程池,但是任务相比线程池有很小的开销和精确的控制,所以Task的控制性和灵活性也是比Thread要好的。
那么在还没有async await语法之前,我们如何解决Thread没有返回值这个问题呢?我们可以运用下面方法,通过委托中的BeginInvoke执行异步代码,然后通过EndInvoke获取异步代码的返回结果,这种异步执行代码的方式可以说是比较灵活了,我们执行异步方法的参数和返回值都是可以变的
static void Main(string[] args) { ThreadMessage("Main Thread"); //建立委托 Func<string, string> fuc = new Func<string, string>(Hello); //异步调用委托,获取计算结果 IAsyncResult result = fuc.BeginInvoke("AsyncWork", null, null); //完成主线程其他工作 Console.WriteLine("主线程执行常规任务....."); //等待异步方法完成,调用EndInvoke(IAsyncResult)获取运行结果 string data = fuc.EndInvoke(result); Console.WriteLine(data); Console.ReadKey(); } static string Hello(string name) { ThreadMessage("Async Thread After Two Seconds"); Thread.Sleep(2000); //模拟异步耗时工作 return "Hello " + name; } //显示当前线程,输出当前线程的线程ID static void ThreadMessage(string data) { string message = string.Format("{0}\n ThreadId is:{1}", data, Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); }
3. async await
前面已经讲了相关知识做铺垫。现在来看看async await 关键字,我们来写一个简单的例子,单纯只使用async关键字不使用 await
static void Main(string[] args) { DoSomethingAsync(); DoSomethingSync(); Console.ReadKey(); } static void DoSomethingSync() { //循环模拟耗时运算 for (int i = 0; i < 1000; i++) { Console.Write("*"); } Console.WriteLine("\nResult: *,我是主线程代码,线程ID:" + Thread.CurrentThread.ManagedThreadId); } static async void DoSomethingAsync() { //循环模拟耗时运算 for (int i = 0; i < 1000; i++) { Console.Write("/"); } Console.WriteLine("\nResult: +,我是async await关键字执行的代码, 线程ID: " + Thread.CurrentThread.ManagedThreadId); }
运行结果如下,可以看到,DoSomethingAsync()和DoSomethingSync()两个函数在同一个线程上同步执行,所以我们得到一个结论,那就是如果一个异步方法只使用async而不使用await,那么这个所谓的异步方法其实和一个普通的方法没有什么区别,并不会异步执行代码
那我们修改上面的代码,在函数里面加上一个await关键字,会有怎样的结果呢,在使用await之前我们有几个需要注意的点
1:await无法等待void,也就是await后面所等待的函数必须要有返回值
2:所有异步函数返回值必须是void,Task和Task<T>类型,有泛型的存在,(自 C# 7后,貌似还可以指定其他任何返回类型,前提是返回类型包含 GetAwaiter
方法。),所以使用async await时我们也不用担心异步代码无法获取返回值的问题
可以看下面代码分别执行普通函数,Thread启动的函数,和用async await的异步函数
static void Main(string[] args) { //async await执行的异步函数 DoSomethingAsync(); //Thread启动的异步函数 DoSomethingAsync2(); //常规同步函数 DoSomethingSync(); Console.ReadKey(); } public static void DoSomethingSync() { //循环模拟耗时运算 for (int i = 0; i < 3000; i++) { Console.Write("*"); } Console.WriteLine("\nResult: *,我是主线程代码,线程ID:" + Thread.CurrentThread.ManagedThreadId); } private static async void DoSomethingAsync() { //此处输出执行的任然是主线程代码 Console.WriteLine("\nResult: +,我是只使用async关键字执行的代码, 线程ID: " + Thread.CurrentThread.ManagedThreadId); //通过await来告诉编译器此处要执行异步代码,所以await后面执行的DoAsync方法是另一线程的异步代码 string msg = await DoAsync(); Console.WriteLine("\nResult: +," + msg); } private static async Task<string> DoAsync() { await Task.Run(() => { //循环模拟耗时运算 for (int i = 0; i < 3000; i++) { Console.Write("+"); } }); return "我是async await关键字执行的异步代码,线程ID:" + Thread.CurrentThread.ManagedThreadId; } private static void DoSomethingAsync2() { //常规Thread启动新的线程 string msg = string.Empty; Thread th = new Thread(() => { //循环模拟耗时运算 for (int i = 0; i < 3000; i++) { Console.Write("-"); } msg = "我是Thread启动的异步代码,线程ID:" + Thread.CurrentThread.ManagedThreadId; Console.WriteLine("\nResult: -," + msg); }); th.Start(); }
其实运行代码我们不难发现,所谓的async await异步函数真正实现异步效果的其实还是Task类,async
和 await
关键字本身并不会创建其他线程,而为什么要用async await,而不是单纯的只使用Task,我的个人看法是如果在异步编程中,用async await配合 Task.Run 将占用大量 CPU 的工作移到后台线程,这样会使代码执行效率相对变高,标记的异步方法使用await来指定暂停点这样写也会使代码相对简洁清晰,我们可以清晰的看的哪里是异步执行的代码,哪里是同步执行的代码,也无需防止争用条件,可以理解为async await是微软对我们异步编程的一种规范。
最后再来实战一个async配合使用委托的代码实战,我们来写一段简化版的代码,来模拟类似asp core框架中间件的调用和注册过程,虽然实际的asp core框架的中间件的调用和注册过程是很复杂的,但是大概代码原理也就和下面差不多,多个中间件管道顺序调用并共同维护同一个HttpContext上下文类
delegate Task RequestDelegate(string context); public class Program { private static readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares = new List<Func<RequestDelegate, RequestDelegate>>(); public static void Main() { //注册中间件 _middlewares.Add(MiddlewareHello); _middlewares.Add(MiddlewareGoodbye); _middlewares.Add(MiddlewareOK); RequestDelegate del = async context => { await Task.CompletedTask; }; _middlewares.Reverse(); //遍历并调用中间件 foreach (var item in _middlewares) { del = item(del); } del("kangkang"); System.Console.ReadKey(); } static RequestDelegate MiddlewareHello(RequestDelegate del) { return async context => { System.Console.WriteLine(" Hello, {0}!", context); await del(context); }; } static RequestDelegate MiddlewareGoodbye(RequestDelegate del) { return async context => { System.Console.WriteLine(" Goodbye, {0}!", context); await del(context); }; } static RequestDelegate MiddlewareOK(RequestDelegate del) { return async context => { System.Console.WriteLine(" SayOK, {0}!", context); }; } }