在上一章节中,为大家介绍了C#多线程(4)——任务并行库TPL,TPL是从.NetFramwork4.0后引入的基于异步操作的一组API,核心关注于任务【 T a s k 和 T a s k < T > \textcolor{red}{Task 和 Task<T>} Task和Task<T>】。简化了我们异步编程的步骤,但在C#5.0时,引入了新的语言特性—— 异步方法 \textcolor{red}{异步方法} 异步方法,是一种语法糖,是TPL之上的更高级别的抽象,它遵循了基于任务的异步模式,仅仅只使用了async与await关键字,更加简化了异步编程,可以避免性能瓶颈并增强应用程序的总体响应能力。
1 异步方法
- 异步方法是用 a s y n c \textcolor{blue}{async} async关键字修饰的方法,通常方法的名称会以Async关键字结尾。官方提供了大量的异步方法。
- 方法的返回值有两种 【 T a s k ,方法体中无 r e t u r n 语句和 T a s k < T > 方法体中 r e t u r n T \textcolor{red}{Task ,方法体中无return语句 和 Task<T> 方法体中return T } Task,方法体中无return语句和Task<T>方法体中returnT】,这个与TPL模型的任务模型是一致的,都是对异步逻辑的封装
- 异步方法通常包含至少一个 await 表达式,该表达式标记一个点,在该点上,直到等待的异步操作完成方法才能继续。
- .NET Framework 4.5 或更高版本以及 .NET Core 包含许多异步方法。 例如,System.IO.Stream 类包含 CopyToAsync、ReadAsync 和 WriteAsync 等方法,以及同步方法 CopyTo、Read 和 Write。
public static async Task<int> GetUrlContentLengthAsync()
{
HttpClient client = new HttpClient();
Task<string> getStringTask = client.GetStringAsync("https://learn.microsoft.com/dotnet");
Console.WriteLine("GetUrlContentLengthAsync 方法中的的Independent work"); //执行不依赖于 GetStringAsync 得出的最终结果的其他工作
string contents = await getStringTask;
return contents.Length; //返回值被包装为Task<string>类型
}
2 async、await原理
使用ILSpy对.dll文件进行进行反编译知,async await是语法糖,底层是 状态机 \textcolor{red} {状态机} 状态机的调用。async标注的方法会被反编译成一个类,会根据await的调用被切分为多个状态,
对async方法的调用会被拆分成MoveNext的调用(根据状态多次进入switch判断体)
3 异步方法与多线程的关系
异步方法旨在成为非阻止操作,即不阻塞异步方法调用方的线程。 \textcolor{red}{异步方法旨在成为非阻止操作,即不阻塞异步方法调用方的线程。} 异步方法旨在成为非阻止操作,即不阻塞异步方法调用方的线程。
await调用的等待期间,.Net会把当前的线程返回给线程池,等异步方法调用执行完成后,框架会从线程池中再取一个线程执行后续的代码(可能与前一个线程相同,也有可能是一个新的线程,这取决于CPU对线程池中线程的调度。await关键字前后可能是不一样的线程)。
线程的切换,主要取决于异步方法中的任务类型及实际CLR对线程的调度:
- I O 密集型 \textcolor{blue}{IO密集型} IO密集型:大部分时间都是cpu在等IO的读写操作。如网络请求数据、访问数据库或读取和写入到文件系统
- C P U 密集型 \textcolor{blue}{CPU密集型} CPU密集型:例如执行成本高昂【耗时长,占用大量内存空间】的计算,
测试 I O 密集型任务, M a i n 方法中关键代码块如下。 \textcolor{red}{测试IO密集型任务, Main方法中关键代码块如下。} 测试IO密集型任务,Main方法中关键代码块如下。
Console.OutputEncoding = Encoding.UTF8;
string des = "E:\\logs\\2.txt";
//定义一个异步方法,从指定的url下载html内容后再写入到本地文件夹中。
async Task DownloadFromUrlAsycn(string filename)
{
using (HttpClient client = new HttpClient())
{
Console.WriteLine("下载前 ,ThreadId is {0},是否为线程池线程{1}", Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread);
//获得url的页面资源 (网络传输,属于IO密集型的任务,此时CPU空闲,为了充分的利用CPU,可能会出现CPU上下文的切换,await前后发现线程的切换)
string s = await client.GetStringAsync("https://learn.microsoft.com/dotnet");
Console.WriteLine("下载后 ,ThreadId is {0},是否为线程池线程{1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
Console.WriteLine("写入文件前 ,ThreadId is {0},是否为线程池线程{1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
//写入执行资源文件
await File.WriteAllTextAsync(filename, s);
Console.WriteLine("写入文件后 ,ThreadId is {0},是否为线程池线程{1}", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
}
}
Console.WriteLine("方法前,ThreadId is {0},是否为线程池线程{1} ", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
await DownloadFromUrlAsycn(des);
Console.WriteLine("方法后,ThreadId is {0},是否为线程池线程{1} ", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
Console.ReadKey();
运行Main方法后,得到下图结果
可以发现,切换线程主要是发生在网络传输和文件写入的前后【IO读写动作】,IO的读写是不需要使用CPU的,只需要发一个命令给硬盘即可,硬盘处理完后会再通知cpu继续处理。为了充分利用CPU在IO读写的阶段,通常会将线程返回到线程池中。该线程可以用于其他的CPU操作。
测试 C P U 密集型任务, M a i n 方法中关键代码块如下。 \textcolor{red}{测试CPU密集型任务, Main方法中关键代码块如下。} 测试CPU密集型任务,Main方法中关键代码块如下。
Console.OutputEncoding = Encoding.UTF8;
Console.WriteLine("方法前线程id为{0}", Thread.CurrentThread.ManagedThreadId);
async Task GetStringAsync() {
Console.WriteLine("方法中线程id为{0}",Thread.CurrentThread.ManagedThreadId);
for (int i = 0; i < 5; i++) {
Thread.Sleep(TimeSpan.FromSeconds(1)); //模拟CPU计算需要耗时
};
};
await GetStringAsync();
Console.WriteLine("方法后线程id为{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("");
Task t= Task.Run(()=>GetStringAsync()) ;
t.Wait();
Console.WriteLine(" Task.Run方法后线程id为{0}", Thread.CurrentThread.ManagedThreadId);
//对于CPU密集型的异步方法时,通常会等待一个使用 Task.Run 方法在后台线程启动的操作。
(1)当是一个CPU密集型的异步方法时,线程的切换涉及到很多资源的消耗,在这种情况下,使用新的线程去执行异步方法的逻辑并不会优于同步执行的逻辑。所以await GetStringAsync()的前后都是使用同一个线程。
(2)综上所述,根据代码的验证我们可以知道异步方法并不能等价于多线程,调用异步方法时,并不是都会通过新的线程去执行异步逻辑,在没有线程切换的状态下【通常是CPU密集型的任务】,与同步方法是一致的。为了达到异步的效果,我们可以使用Task.Run()的方法,在后台线程中执行异步操作。