为什么要使用异步编程
性能优化:
主线程利用率:异步编程允许主线程持续运行多个任务,避免因子线程阻塞而降低性能。
I/O密集型任务处理:异步编程特别适合处理大量I/O操作的任务,如文件读写、数据库查询等,这些操作可以独立于主线程运行,提升应用响应速度。
可扩展性:
多处理器支持:异步编程能够充分利用多处理器资源,每个CPU核同时处理不同的任务,从而提高系统的性能和稳定性。
高负载请求处理:在处理大量并发请求时,同步编程可能导致主线程被阻塞,而异步编程则能够有效地分担压力。
现代Web开发趋势:
技术兼容性:与主流Web框架(如React、Vue)的集成更加顺畅,使用async/await关键字可以简化代码,提高开发效率。
用户体验提升:异步编程通过延迟加载等技术减少用户等待时间,提升应用体验。
资源利用效率:
多任务并行处理:主线程可以通过多线程或协 ordinates 执行多个任务,避免资源浪费,提高系统的吞吐量。
趋势适应性:
异步编程的流行:在现代应用开发中,异步编程因其简洁性和高效性而备受青睐,采用该模式可以与现有技术栈更好地结合。
创建异步方法
1 2 3 4 5 6 7 8 9 10 11 12 13
| static void Main(string[] args) { await DownloadHTmlAsync("http://www.ptpress.com.cn", @"E:\temp\1.txt"); } static async Task DownloadHTmlAsync(string url,string filename) { using (HttpClient httpClient = new HttpClient()) { string html = await httpClient.GetStringAsync(url); await File.WriteAllTextAsync(filename, html); } }
|
带返回值的
获取长度
1 2 3 4 5 6 7 8 9 10 11 12 13
| static async Task Main(string[] args) { int I = await DownloadHTmlAsync("http://www.ptpress.com.cn", @"E:\temp\1.txt"); } static async Task<int> DownloadHTmlAsync(string url,string filename) { using (HttpClient httpClient = new HttpClient()) { string html = await httpClient.GetStringAsync(url); await File.WriteAllTextAsync(filename, html); return html.Length; } }
|
HttpClient: 提供一个类,用于从 URI 标识的资源发送 HTTP 请求和接收 HTTP 响应。
Flie: 提供用于创建、复制、删除、移动和打开单个文件的静态方法,并有助于创建 FileStream 对象。
GetStringAsync: 将 GET 请求发送到指定 URI 并在异步操作中以字符串的形式返回响应正文。
创建一个自定义的异步任务类:
创建一个继承自Task< T >的类,并在方法中添加[System.Threading.Await]或使用async关键字。
下载HTML内容:
在异步方法中,使用HttpClient来获取指定URL的HTML内容。这一步是非阻塞操作,不会阻塞主线程。
将HTML内容保存到文件:
使用File.WriteAllTextAsync将获取到的HTML内容写入指定的目标文件路径,并确保资源被正确关闭。
async 线程切换
理解关键字“async”与“await”:
async用于定义一个可以执行异步操作的任务。
await用于等待该任务的完成。
线程切换的基本概念:
线程切换是程序运行时从一个线程切换到另一个线程的过程。
使用“async”和“await”,C#在主线程和异步任务之间自动进行线程切换。
await触发的线程切换过程:
当调用带有async关键字的任务,并使用await等待其完成时,C#会暂停主线程。
创建一个新的后台线程来执行该异步任务。
直到该后台线程完成或抛出异常,主线程才会重新启动。
嵌套使用“await”的情况:
在一个async任务内部再次调用另一个带有async和await的任务时,C#会创建新的线程并切换主从线程。
这种多层嵌套增强了代码的可读性和灵活性。
同步与错误处理:
await操作会阻塞主线程直到所有等待的任务完成或出现异常。
如果某个任务抛出异常,后续的所有await操作都会终止,并按照错误处理机制进行。
潜在的问题与优化:
当多个任务需要阻塞主线程时,可能会导致主线程被频繁切换,影响性能。这种情况下,可以考虑使用异步队列或其他结构来提高效率。
使用适当的同步和错误处理机制可以避免资源泄漏和其他潜在问题。
总结:
“async”与“await”通过自动管理线程切换,简化了编写异步操作的代码,提高了开发效率。
正确理解和使用这些关键字能够有效提升程序的性能和可维护性。
await调用的等待期间,.NET会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续代码(不一定时同一个线程池)
验证:在耗时异步(写入打字符串)操作前后分别打印线程Id
Thread.CurrentThread.ManagedThreadId获得当前线程Id。
1 2 3 4 5 6 7 8 9 10 11 12
| static async Task Main(string[] args) { Console.WriteLine(Thread.CurrentThread.ManagedThreadId); StringBuilder sb= new StringBuilder(); for(int i = 0; i < 10000; i++) { sb.Append("youxianyu"); } await File.WriteAllTextAsync(@"E:\temp\1.txt", sb.ToString()); Console.WriteLine(Thread.CurrentThread.ManagedThreadId); }
|
总结:编译器把async拆分成多次方法调用,程序在运行的时候会通过线程池中取出空闲线程执行不同MoveNext调用的方法来避免线程的“空等”,从而提升系统的并发处理能力。
异步方法不等于多线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| static async Task Main(string[] args) { Console.WriteLine("之前:" + Thread.CurrentThread.ManagedThreadId); double r=await CalcAsync(500); Console.WriteLine($"r={r}"); Console.WriteLine("之后:" + Thread.CurrentThread.ManagedThreadId); } public static async Task<double> CalcAsync(int n) { Console.WriteLine("CalcAsync:" + Thread.CurrentThread.ManagedThreadId); double result = 0; Random rand = new Random(); for (int i = 0; i < n * n; i++) { result += rand.NextDouble(); } return result; }
|
异步方法的代码并不会自动在新线程中执行,除非把代码放到新线程中执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| static async Task Main(string[] args) { Console.WriteLine("之前:" + Thread.CurrentThread.ManagedThreadId); double r=await CalcAsync(500); Console.WriteLine($"r={r}"); Console.WriteLine("之后:" + Thread.CurrentThread.ManagedThreadId); } public static async Task<double> CalcAsync(int n) { return await Task.Run(() => { Console.WriteLine("CalcAsync:" + Thread.CurrentThread.ManagedThreadId); double result = 0; Random rand = new Random(); for (int i = 0; i < n * n; i++) { result += rand.NextDouble(); } return result; });
|
可以看到Task.Run的线程ID和Main方法中的线程ID不同,这说明Task.Run中的代码被放到新线程中执行了。
为什么有的异步方法没标async
有async
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| static async Task Main(string[] args) { string s1=await ReadFileAsync(1); Console.WriteLine(s1); } static async Task<string> ReadFileAsync(int num) { switch (num) { case 1: return await File.ReadAllTextAsync("e:/temp/1.txt"); case 2: return await File.ReadAllTextAsync("e:/temp/2.txt"); default: throw new ArgumentException("num参数不正确"); } }
|
没有async
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| static Task Main(string[] args) { string s1=await ReadFileAsync(1); Console.WriteLine(s1); } static Task<string> ReadFileAsync(int num) { switch (num) { case 1: return File.ReadAllTextAsync("e:/temp/1.txt"); case 2: return File.ReadAllTextAsync("e:/temp/2.txt"); default: throw new ArgumentException("num参数不正确"); } }
|
区别:
由于ReadFileAsync方法没有用async关键字修饰,因此我们没有看到ReadFileAsync方法的代码编译生成的类,ReadFileAsync内部也不像async方法那样复杂,而只是简单地调用File.ReadAllTextAsync方法。ReadFileAsync方法没有编译生成类,因此不会增加程序集的尺寸,而且运行效率更高。因此,如果一个异步方法只是对别的异步方法进行简单的调用,并没有太多复杂的逻辑,比如获取异步方法的返回值后再做进一步的处理,就可以去掉async、await关键字。
不要用sleep
如果想在异步编程方法中暂停一段时间,不要用Thread.Sleep(),因为它会阻塞调用线程,而要用await Task.Delay()。

封装一个异步方法,下载给指定网址,如果下载失败,则稍等500ms再重试,如果重试三次仍然失败,则抛异常“下载失败”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| static async Task Main(string[] args) { int sum = await ReadFileAsync("http://www.baidu.com"); Console.WriteLine(sum); } static public async Task<string> ReadFileAsync(string url, int init=0) { try { using (var httpClient = new HttpClient()) { var html = await httpClient.GetStringAsync(url); return html; } } catch (Exception e) { init++; if (init >= 3) { throw new Exception("请求失败"); } await Task.Delay(5000); await ReadFileAsync(url, init); return "下载失败"; }
} }
|
用于获得提前终止执行的信号
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| static async Task Main(string[] args) { CancellationTokenSource cts = new CancellationTokenSource(); cts.CancelAfter(3000); CancellationToken cToken= cts.Token; await DownLoadAsync("http://www.baidu.com", 10,cts.Token); } static async Task DownLoadAsync(string url, int n, CancellationToken cancellationToken) { using (HttpClient client = new HttpClient()) { for (int i = 0; i < n; i++) { string html = await client.GetStringAsync(url); Console.WriteLine($"{DateTime.Now}:{html}"); if (cancellationToken.IsCancellationRequested) { Console.WriteLine("请求被取消"); break; } } } }
|
CancellationTokenSource:向 CancellationToken 发出应取消的信号。
CancelAfter:在此 CancellationTokenSource 上计划取消操作。
CancellationToken:传播应取消作的通知。
IsCancellationRequested:获取是否已为此令牌请求取消。
whenAll
创建一个任务,该任务将在所有提供的任务完成时完成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| static async Task Main(string[] args) {
string[] files=Directory.GetFiles(@"E:\temp"); Task<int>[] countTasks=new Task<int>[files.Length]; for(int i = 0; i < files.Length; i++) { string filename = files[i]; Task<int> t= ReadCharsCount(filename); countTasks[i] = t; } int[] counts=await Task.WhenAll(countTasks); int c=counts.Sum(); Console.WriteLine(c); } static async Task<int> ReadCharsCount(string filename) { string s=await File.ReadAllTextAsync(filename); return s.Length; }
|
GetFiles:返回满足指定条件的文件的名称。
Directory:公开用于通过目录和子目录进行创建、移动和枚举的静态方法。 此类不能被继承。