勇哥注:
想了解异步编程,大家可以从下面这两方面入手: (1)生活当中异步处理的例子 注意跟异步跟中断处理的区别 (2)编程方面的应用 1。I/O操作 2。UI交互 3。计算密集型任务 4。 并行编程 异步操作主要用于提高应用程序的响应性能和处理能力
控制台的一个小例子
这个例子让你快速了解C#异步编程中 async,await, 关键字的用法。
注意:
1。如果启用注释的语句 t1.Wait(),会阻塞只到fun1()执行完成才会执行后面的语句。这样你可以看到同步代码的效果。
2。程序运行后,你按一次任何键后,才可以看到异步返回的结果 “async ok”
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace 异步编程1_ansyc_await { class Program { static void Main(string[] args) { Task<string> t1 = fun1(); //t1.Wait(); ExtraOperation(1); Console.WriteLine($"ExtraOperation ok"); Console.ReadKey(); Console.WriteLine(t1.Result); Console.ReadKey(); } private static async Task<string> fun1() { Console.WriteLine("异步操作开始"); await Delay(2000); Console.WriteLine("异步操作完成"); return "async ok"; } private static Task Delay(int m) { Stopwatch s = new Stopwatch(); s.Start(); var t1 = Task.Run(() => { while (true) { Thread.Sleep(1); if (s.ElapsedMilliseconds > m) break; } }); return t1; } /// <summary> /// 额外操作 /// </summary> /// <param name="id"></param> private static void ExtraOperation(int id) { //这里是通过拼接字符串进行一些相对耗时的操作 var s = ""; for (var i = 0; i < 6000; i++) { s += i; } Console.WriteLine($"id = {id} 的 ExtraOperation 方法完成"); } } }
另一个例子,做早餐:
(一)同步做早餐
顺序动作是:
倒咖啡
加热平底锅
煎鸡蛋
煎培根
烤面包
面包涂果酱
倒果汁
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 异步编程_做早餐 { //同步版本 class Program { /* 倒咖啡 加热平底锅 煎鸡蛋 煎培根 烤面包 面包涂果酱 倒果汁 */ private static int time = 0; static void Main(string[] args) { 顺序执行的早餐(); Console.ReadKey(); } private static void 顺序执行的早餐() { time = 0; Stopwatch watch = Stopwatch.StartNew(); watch.Start(); Console.WriteLine($"{倒咖啡()}ok"); Console.WriteLine($"{煎鸡蛋()}ok"); Console.WriteLine($"{煎培根()}ok"); Console.WriteLine($"{烤面包()}ok"); Console.WriteLine($"{面包涂果酱()}ok"); Console.WriteLine($"{倒果汁()}ok"); watch.Stop(); Console.WriteLine($"早餐总时间:{watch.Elapsed.TotalSeconds.ToString("0.00")}"); } private static string 倒咖啡() { time += 1; Console.WriteLine("倒咖啡..."); return "倒咖啡"; } private static string 加热平底锅() { time += 2; Console.WriteLine("加热平底锅..."); return "加热平底锅"; } private static string 煎鸡蛋() { time += 5; 加热平底锅(); Task.Delay(3000).Wait(); Console.WriteLine("加热平底锅ok"); Console.WriteLine("煎鸡蛋..."); Task.Delay(3000).Wait(); return "煎鸡蛋"; } private static string 煎培根() { time += 6; Console.WriteLine("煎培根..."); Task.Delay(6000).Wait(); return "煎培根"; } private static string 烤面包() { time += 10; Console.WriteLine("烤面包..."); Task.Delay(3000).Wait(); return "烤面包"; } private static string 面包涂果酱() { time += 5; Console.WriteLine("面包涂果酱..."); return "面包涂果酱"; } private static string 倒果汁() { time += 1; Console.WriteLine("倒果汁..."); return "果汁"; } } }
(二)改成异步
这个异步的版本,你会观察到做早餐的时间是一样的。
区别是,如果这个程序是winform的话,你会发现UI没有卡住,可以操作。
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 异步编程_做早餐2 { //异步版本,但是本版本的时间是一样的 //区别是,如果这个程序是winform的话,你会发现UI没有卡住,可以操作。 class Program { static void Main(string[] args) { 异步做早餐(); Console.ReadKey(); } private static async void 异步做早餐() { Stopwatch watch = Stopwatch.StartNew(); watch.Start(); Console.WriteLine($"{倒咖啡()}ok"); Console.WriteLine($"{await 煎鸡蛋()}ok"); Console.WriteLine($"{await 煎培根()}ok"); Console.WriteLine($"{await 烤面包()}ok"); Console.WriteLine($"{面包涂果酱()}ok"); Console.WriteLine($"{倒果汁()}ok"); watch.Stop(); Console.WriteLine($"早餐总时间:{watch.Elapsed.TotalSeconds.ToString("0.00")}"); } private static string 倒咖啡() { Console.WriteLine("倒咖啡..."); return "倒咖啡"; } private static string 加热平底锅() { Console.WriteLine("加热平底锅..."); return "加热平底锅"; } private async static Task<string> 煎鸡蛋() { 加热平底锅(); await Task.Delay(3000); Console.WriteLine("加热平底锅ok"); Console.WriteLine("煎鸡蛋..."); await Task.Delay(3000); return "煎鸡蛋"; } private async static Task<string> 煎培根() { Console.WriteLine("煎培根..."); await Task.Delay(6000); return "煎培根"; } private async static Task<string> 烤面包() { Console.WriteLine("烤面包..."); await Task.Delay(3000); return "烤面包"; } private static string 面包涂果酱() { Console.WriteLine("面包涂果酱..."); return "面包涂果酱"; } private static string 倒果汁() { Console.WriteLine("倒果汁..."); return "果汁"; } } }
(三)优化异步版本
我们可以在开始一个任务后,在等待任务完成时,继续准备其它的任务。
你也几乎将在同一时间完成所有工作。你将吃到顿热气腾腾的早餐。
通过合并任务和调整任务的顺序,将大大节约任务的完成时间。
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 异步编程_做早餐3 { //优化异步 class Program { static void Main(string[] args) { 优化异步做早餐(); Console.ReadKey(); } private static async void 优化异步做早餐() { Stopwatch watch = Stopwatch.StartNew(); watch.Start(); Console.WriteLine($"{倒咖啡()}ok"); var eggTask = 煎鸡蛋(); var pgTask = 煎培根(); var totalTask = 烤面包涂果酱二合一任务(); Console.WriteLine($"{await eggTask}ok"); Console.WriteLine($"{await pgTask}ok"); Console.WriteLine($"{await totalTask}ok"); Console.WriteLine($"{倒果汁()}ok"); watch.Stop(); Console.WriteLine($"早餐总时间:{watch.Elapsed.TotalSeconds.ToString("0.00")}"); } private static async Task<string> 烤面包涂果酱二合一任务() { var ast = await 烤面包(); 面包涂果酱(); return ast; } private static string 倒咖啡() { Console.WriteLine("倒咖啡..."); return "倒咖啡"; } private static string 加热平底锅() { Console.WriteLine("加热平底锅..."); return "加热平底锅"; } private async static Task<string> 煎鸡蛋() { 加热平底锅(); await Task.Delay(3000); Console.WriteLine("加热平底锅ok"); Console.WriteLine("煎鸡蛋..."); await Task.Delay(3000); return "煎鸡蛋"; } private async static Task<string> 煎培根() { Console.WriteLine("煎培根..."); await Task.Delay(6000); return "煎培根"; } private async static Task<string> 烤面包() { Console.WriteLine("烤面包..."); await Task.Delay(3000); return "烤面包"; } private static string 面包涂果酱() { Console.WriteLine("面包涂果酱..."); return "面包涂果酱"; } private static string 倒果汁() { Console.WriteLine("倒果汁..."); return "果汁"; } } }
通过合并任务和调整顺序,做一份早餐,节省了一大半的时间。
请看下面优化后的示意图:
(四)异步异常
因为是异步产生的异常,因此机制和同步的时候有区别的。
有两个重要机制:
1。异常在出错的任务中的存储方式
2。 在代码等待出错的任务时解包并重新引发异常的方式
异常步运行的代码引发异常时,该异常存储在Task中。
Task.Exception属性为System.AggregateException,因为异常工作期间可能会引发多个异常。
引发的任何异常都将添加到AggregateException.InnerExceptions集合中。
如果该Exception属性为Null,则将创建一个新的AggregateException且引发的异常是该集合中的第一项。
对于出错的任务,最觉见的情况是Exception属性只包含一个异常。
当awawit出错的任务时,将重新引发AggreateException.InnerExceptions集合中的第一个异常。因此,此示例的输出显示
InvalidOperationException,而不是AggregateException。提取第一个内部异常使得异步方法与使用其对应的同步方法尽可能相似。
当你的场景可能生成多个异常时,可以在代码中检查Exception属性。
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; namespace 异步编程_做早餐时失业处理异常 { //异步异常的处理 class Program { static void Main(string[] args) { 优化异步做早餐(); Console.ReadKey(); } private static async void 优化异步做早餐() { try { Stopwatch watch = Stopwatch.StartNew(); watch.Start(); Console.WriteLine($"{倒咖啡()}ok"); var eggTask = 煎鸡蛋(); var pgTask = 煎培根(); var totalTask = 烤面包涂果酱二合一任务(); Console.WriteLine($"{await eggTask}ok"); Console.WriteLine($"{await pgTask}ok"); Console.WriteLine($"{await totalTask}ok"); Console.WriteLine($"{倒果汁()}ok"); watch.Stop(); Console.WriteLine($"早餐总时间:{watch.Elapsed.TotalSeconds.ToString("0.00")}"); } catch(AggregateException ex) { Console.WriteLine($"线程内部异常,{ex.StackTrace}"); } catch(Exception ex) { Console.WriteLine($"其它异常,{ex.Message}"); } } private static async Task<string> 烤面包涂果酱二合一任务() { var ast = await 烤面包(); 面包涂果酱(); return ast; } private static string 倒咖啡() { Console.WriteLine("倒咖啡..."); return "倒咖啡"; } private static string 加热平底锅() { Console.WriteLine("加热平底锅..."); return "加热平底锅"; } private async static Task<string> 煎鸡蛋() { 加热平底锅(); await Task.Delay(3000); Console.WriteLine("加热平底锅ok"); Console.WriteLine("煎鸡蛋..."); await Task.Delay(3000); return "煎鸡蛋"; } private async static Task<string> 煎培根() { Console.WriteLine("煎培根..."); await Task.Delay(6000); return "煎培根"; } private async static Task<string> 烤面包() { Console.WriteLine("烤面包..."); await Task.Delay(3000); Console.WriteLine("着火了!"); int a = 1, b = 0; int i = a / b;//制造一个异常 return "烤面包"; } private static string 面包涂果酱() { Console.WriteLine("面包涂果酱..."); return "面包涂果酱"; } private static string 倒果汁() { Console.WriteLine("倒果汁..."); return "果汁"; } } }
(五)高效的等待
Task.WhenAll的用法
var eggTask = 煎鸡蛋(); var pgTask = 煎培根(); var totalTask = 烤面包涂果酱二合一任务(); await Task.WhenAll(eggTask, pgTask, totalTask); Console.WriteLine($"煎鸡蛋ok"); Console.WriteLine($"煎培根ok"); Console.WriteLine($"烤面包ok");
另一种是WhenAny,它将返回一个当其参数完成时才完成的Task<>。
//第二种 var allTasks = new List<Task> { eggTask, pgTask, totalTask }; while(allTasks.Count>0) { var res = await Task.WhenAny(allTasks); if(res==eggTask) { Console.WriteLine($"煎鸡蛋ok"); } else if(res==pgTask) { Console.WriteLine($"煎培根ok"); } else if(res== totalTask) { Console.WriteLine($"烤面包ok"); } allTasks.Remove(res); }
最后,勇哥和AI的一次对话,放出来做为总结:C#的 async, await 异步到底是什么?传统线程的方式是不是都可以用异步替换?
勇哥问:
C# 异步不是 await 行待一个task运行结果吗? 那它的本质是不是启动了一个后台线程。
因此我认为异步其实是线程的一种用法,只是C#通过语法糖做了一层封装,让异步编程更容易了。这样理解对吗
AI回答:
您的理解基本正确。在C#中,异步编程可以看作是对后台线程的一种封装和抽象。使用异步编程可以让我们更方便地处理耗时操作,而无需显式地创建和管理线程。
异步编程的本质是异步执行一个任务,这意味着在主线程中启动一个任务后,主线程可以继续执行其他操作,而不需要等待这个任务完成。当异步任务完成时,它会通知主线程并返回结果。
在C#中,可以使用async
和await
关键字来简化异步编程。通过async
关键字可以定义一个异步方法,然后在方法内部使用await
关键字来等待一个异步任务的完成。这种方式实际上是在背后使用了一个线程池来处理异步任务,并对线程进行了封装和抽象。
因此,可以说异步编程是C#中对后台线程的一种封装和抽象,它提供了一种更方便的方式来处理耗时操作,无需显式地创建和管理线程。同时,使用async
和await
关键字可以让我们更清晰地表达异步操作的流程和逻辑,降低了异步编程的复杂性。
勇哥问:
既然异步是由C#介入了管理,让用户不用显式的管理线程。那是不是意味着,
我们在原来使用Thread,或者是Task的场合,都可以改用async和await这样的异步方式来使用。
AI回答:
是的,在很多情况下,我们可以将原来使用Thread或Task的场合改为使用async和await这样的异步方式来处理。异步编程提供了一种更简洁、更直观的方式来处理耗时操作,而无需显式地创建和管理线程。使用async和await关键字,我们可以将异步操作表示为一系列连续的步骤,每个步骤都可以在等待上一个步骤完成时执行其他操作。这种方式可以使代码更加清晰和易于理解,并且可以减少线程管理和同步的复杂性。
然而,需要注意的是,并不是所有的情况都适合使用异步编程。
在某些特定场合下,更适合使用传统的线程(Thread)而不是异步编程(async)。以下是一些可能的情况:
高并发和并行计算:当需要同时执行多个独立的任务,并且要求更高的并发性能时,使用线程池和Task可能更为高效。线程池可以更好地管理线程的生命周期,避免频繁创建和销毁线程的开销,并且可以更灵活地控制线程池的大小。
精确控制线程行为:在某些需要精确控制线程行为的场景下,如实时系统或高性能计算,使用Thread可能更为合适。这些场景通常需要对线程进行精细管理和同步,以确保系统的稳定性和性能。
复杂的UI交互:在一些复杂的UI交互场景下,使用Thread可能更为合适。例如,当需要在UI线程上执行一些长时间运行的操作,并且需要频繁更新UI元素时,使用Thread可以避免阻塞UI线程,提高响应性能。
t.Wait()
和await t的区别:
在C#中,t.Wait()
和await t
都是等待异步任务完成的方法,但它们之间有一些重要的区别。
t.Wait()
:Wait()
方法会阻塞当前线程,直到任务t
完成。这意味着当前线程会停止执行,并等待任务
t
完成。使用
Wait()
方法时,必须确保不在主UI线程上调用它,否则会导致应用程序界面冻结。await t;
:await
关键字用于异步等待任务。使用
await
时,当前方法会暂停执行,等待任务t
完成。等待期间,不会阻塞当前线程。这意味着其他线程可以继续执行。
在UI应用程序中,使用
await
可以避免应用程序界面冻结。
总结:
t.Wait()
会阻塞当前线程,直到任务完成。await t
不会阻塞当前线程,允许其他线程继续执行。
在UI应用程序中,通常推荐使用await
来等待异步任务,因为这样可以保持应用程序的响应性。
而在后台线程中,如果没有UI要求,可以使用Wait()
方法来等待任务完成。
勇哥2023.12.16注:
异步跟线程究竟有什么区别?这个问题我再探讨一下。
(一)在异步函数中,await 等待一个后台线程,并且会释放cpu的使用。这一点对于winform程序非常有用。
可以解决UI卡死的问题。
下面代码输出是先 begin,再ok
看上去是同步执行的效果,但实际上如果你此时移动窗口,会发现UI没有被卡死。
await Task.Delay(3000) 这一句虽然是等待,但此时是释放了cpu的使用。
private async void button1_Click(object sender, EventArgs e) { Console.WriteLine($"begin"); await Task.Delay(3000); Console.WriteLine($"ok"); }
如果换成下面的代码:
Task.Delay(3000).Wait() 则是同步代码,此时你尝试移动窗口,会发现UI被卡死,等3秒后,才活过来。
这个效果,如果你替换为Thead.sleep(3000) 是一样的。
private async void button1_Click(object sender, EventArgs e) { Console.WriteLine($"begin"); Task.Delay(3000).Wait(); Console.WriteLine($"ok"); }
因此,对于一个异步线程来说,它即可以异步调用,也可以同步方式调用。
(二) 一例子再来解释一下什么是异步
收银员处理客人A的商品,正在扫码商品时,客人A说还需要一盒蚊香,于是收银员让同事去拿一盒蚊香(通知异步调用),此时继续扫码商品(不会打断主任务),所有商品都扫码完成了,就差蚊香了,于是收银员进行等待并最后扫码蚊香。
下面勇哥写个例程:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace WindowsFormsApp1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private async void button1_Click(object sender, EventArgs e) { await 收银动作(); } private async Task<int> 收银动作() { 扫码商品("苹果1公斤"); 扫码商品("鸡蛋6个"); var task1= 通知同事拿一盒蚊香(); 扫码商品("洋葱1个"); 扫码商品("面包一袋"); 扫码商品("香菜一袋"); await task1; return 0; } private Task 通知同事拿一盒蚊香() { var f1= Task.Run(async () => { await Task.Delay(5000); 扫码商品("蚊香一盒"); }); return f1; } private void 扫码商品(string name) { Console.WriteLine($"{name} 结账"); } } }
输出结果:
苹果1公斤 结账 鸡蛋6个 结账 洋葱1个 结账 面包一袋 结账 香菜一袋 结账 //最后的蚊香是等待6秒后才输出 蚊香一盒 结账
这个过程我们可以看到两点:
(1)通知同事去拿蚊香不影响主流程
(2)我们可以在适当的位置等结果(异步等待的位置你可以自由决定)
而且这个await,上面我们已经说过,它在等待时是释放cpu的,这个跟同步等待是完全不同的。
另外,你可以自由的在await t(异步等待) 和 t.wait (同步等待) 之间切换。
如果你使用后台线程来做同样的事,你可以做到(1),但是你非常不容易做到(2)
以上仅是异步编程好处的一小部分,关于在关键字async和await出现之前,异步编程的难点在哪里?
下面勇哥详细总结一下:
在C#中,异步编程的难点主要在于以下几个方面,在没有使用async和await关键字之前: 回调地狱(Callback Hell):在没有async和await之前,异步编程通常使用回调函数来处理异步操作。 这会导致代码结构变得复杂,难以阅读和维护。 回调地狱是指代码中嵌套多个回调函数,使得代码难以理解和调试。 异常处理:在异步编程中,异常处理是一个重要的问题。如果在异步操作中抛出异常,而没有正确处理, 可能会导致程序崩溃或出现不可预料的行为。在传统的异步编程模型中, 需要特别小心地处理异常,确保它们不会在回调函数中被忽略。 资源管理:异步操作通常涉及到资源的管理,如网络连接、文件句柄等。 在传统的异步编程模型中,需要手动管理这些资源,确保它们在使用后被正确释放。 这增加了代码的复杂性,并可能导致资源泄漏的问题。 并发问题:异步编程涉及到并发问题,如竞态条件和死锁等。在传统的异步编程模型中, 需要小心地设计代码以避免这些问题,这增加了编程的难度。 同步和异步的转换:在传统的异步编程模型中,需要手动将同步代码转换为异步代码, 这增加了编程的复杂性。同时,也需要手动将异步代码转换回同步代码,以确保代码的正确执行。 代码可读性:由于传统异步编程模型的结构复杂,代码的可读性通常较差。 这使得代码难以理解和维护。 通过使用async和await关键字,C#提供了一种更简单、更直观的异步编程模型。 这些关键字使得异步代码看起来像同步代码,减少了回调地狱的出现, 简化了异常处理和资源管理,并提高了代码的可读性。 因此,使用async和await关键字可以大大简化异步编程的难度。
正是微软整了这两个关键字 async,await,让曾经少数人能掌握的异步编程,变成非常容易使用,甚至人人都可以掌握。
---------------------
作者:hackpig
来源:www.skcircle.com
版权声明:本文为博主原创文章,转载请附上博文链接!

