一、什么是异步
启动程序时,系统会在内存中创建一个新的进程。
进程: 构成运行程序的资源的集合。这些资源包括虚地址空间、文件句柄和程序运行所需的其他许多东西。
在进程内部,系统创建了一个称为线程的内核对象,它代表了真正执行的程序。(线程是“执行线程”的简称。)一旦进程建立,系统会在 Main 方法的第一行语句处开始线程的执行。
关于线程:
- 默认情况下,一个进程只包含一个线程(主线程),从程序的开始一直执行到结束。
- 线程可以派生其他线程(子线程),因此在任意时刻,一个进程都可能包含不同状态的多个线程,它们执行程序的不同部分。
- 如果一个进程拥有多个线程,它们将共享进程的资源。
- 系统为处理器执行所调度的单元是线程,不是进程。
不使用异步的示例:
在我的电脑上,书上的示例中 CountCharacters(1, “htttp://microsoft.com” ); 里调用 DownloadString 方法会卡很久,可能传入的这个微软官方网的服务器有问题。(经我经验所知,微软官方网在某些时候就调用加载这一步时常会变得很慢,尤其登录微软账号的时候,显得更明显)于是我就直接换了个百度首页的地址来测试。
class MyDownloadString{Stopwatch sw = new Stopwatch();const int LargeNumber = 6000000;public void DoRun(){sw.Start();int t1 = CountCharacters(1, "https://www.baidu.com/");int t2 = CountCharacters(2, "http://www.illustratedcsharp.com");CountToALargeNumber(1);CountToALargeNumber(2);CountToALargeNumber(3);CountToALargeNumber(4);Console.WriteLine($"Chars in https://www.baidu.com/ :{t1}");Console.WriteLine($"Chars in http://www.illustractedcsharp.com :{t2}");}//获取网页源代码内容的长度private int CountCharacters(int id,string uriString){WebClient wc1 = new WebClient();//执行此处记录下时间值Console.WriteLine("Starting call {0} :{1, 4:N0} ms",id, sw.Elapsed.TotalMilliseconds);//需要时间下载并获取网页源代码string result = wc1.DownloadString(new Uri(uriString));Console.WriteLine("Call {0} completed:{1, 4:N0}ms",id, sw.Elapsed.TotalMilliseconds);return result.Length;}//每间隔一次时间,打印结果。//该方法在书上是有两个形参的,//但是我觉得有些多余(可能作者为了使代码规范化吧)//我为了简便就只使用一个形参private void CountToALargeNumber(int id){//循环作用:为了让程序执行一段时间,才打印一次结果,以方便查看计数值//如果不执行循环,以下打印结果很可能都是相同值。for (long i = 0; i < LargeNumber; i++);Console.WriteLine("End counting {0}: {1,4:N0}ms",id, sw.Elapsed.TotalMilliseconds);}}class Program{static void Main(string[] args){MyDownloadString ds = new MyDownloadString();ds.DoRun();Console.ReadKey();}}
输出结果:
Starting call 1 : 1 ms
Call 1 completed: 694ms
Starting call 2 : 694 ms
Call 2 completed: 3,147ms
End counting 1 : 3,188ms
End counting 2 : 3,235ms
End counting 3 : 3,280ms
End counting 4 : 3,323ms
Chars in https://www.baidu.com/ :9269
Chars in http://www.illustractedcsharp.com :5164
由输出结果可知,执行 DoRun 方法时,该方法里的代码都是按照顺序来执行的。也就是说,每调用一行代码之前,都得要等待上一行代码执行完毕之后才能执行。这里的示例中,由于 调用 DownloadString 方法是需要请求网络时间的, 所以需要等待 DownloadString 方法执行完毕之后,才能执行下一行代码。
使用异步的示例
根据上面的示例,把CountCharacters 方法重写为异步调用的方法:
把 DownloadString 改为 DownloadStringTaskAsync,即改为异步调用方法。
class MyDownloadString{Stopwatch sw = new Stopwatch();const int LargeNumber = 6000000;public void DoRun(){sw.Start();//此处代码修改Task t1 =CountCharacters(1, "https://www.baidu.com/");Task t2 = CountCharacters(2, "http://www.illustratedcsharp.com");CountToALargeNumber(1);CountToALargeNumber(2);CountToALargeNumber(3);CountToALargeNumber(4);Console.WriteLine($"Chars in https://www.baidu.com/ :{t1.Result}");//此处代码修改Console.WriteLine($"Chars in http://www.illustractedcsharp.com :{t2.Result}");//此处代码修改}//获取网页源代码内容的长度,此处代码修改private async Task CountCharacters(int id, string uriString){WebClient wc1 = new WebClient();//执行此处记录下时间值Console.WriteLine("Starting call {0} :{1, 4:N0} ms",id, sw.Elapsed.TotalMilliseconds);//需要时间下载并获取网页源代码var result = await wc1.DownloadStringTaskAsync(new Uri(uriString));//此处代码修改wc1.DownloadString(new Uri(uriString));Console.WriteLine("Call {0} completed:{1, 4:N0}ms",id, sw.Elapsed.TotalMilliseconds);return result.Length;}//每间隔一次时间,打印结果private void CountToALargeNumber(int id){//循环作用:为了让程序执行一段四件,才打印一次结果,以方便查看计数值//如果不执行循环,以下打印结果很可能都是相同值。for (long i = 0; i < LargeNumber; i++);Console.WriteLine("End counting {0}: {1,4:N0}ms",id, sw.Elapsed.TotalMilliseconds);}}class Program{static void Main(string[] args){MyDownloadString ds = new MyDownloadString();ds.DoRun();Console.ReadKey();}}
输出结果:
Starting call 1 : 6 ms
Starting call 2 : 237 ms
End counting 1 : 287ms
End counting 2 : 342ms
Call 1 completed: 384ms
End counting 3 : 394ms
End counting 4 : 440ms
Chars in https://www.baidu.com/ :9269
Call 2 completed: 925ms
Chars in http://www.illustractedcsharp.com :5164
由输出结果可知,程序执行到 DownloadStringTaskAsync 方法时,程序则以异步调用的方式进行。这样可以执行 DownloadStringTaskAsync 的同时,又可以执行下一行代码。
但是,最后两行代码,输出的参数从 t1 改为 t1.Result (从 t2 改为 t2.Result),由于Result 为 Task 类型的,所以这两行代码需要等待 DownloadStringTaskAsync执行完毕后,返回其值才能被输出。如果该方法没有执行完毕,则阻塞并等待期值到来。
以上例子使用了异步调用,但是没有开辟新线程。是因为程序主线程异步调用了 DownloadStringTaskAsync 告诉系统它想要获取资源后,然后继续做自己的事情了。如果中间过程中,在后台系统请求网络得出结果后,会告诉主线程并把结果返回给它,它就停止手上的工作继续处理这个事情,处理完后再继续执行之前手上的工作。
由以上两个示例的输出结果可知,如果方法里有需要等待的代码块,那么该方法为同步方法时就会比作为异步方法时,需要耗费的时间就比较长。
(不使用异步示例中,该方法执行的整个过程所耗费的时间为3,323ms;而使用异步后,需要时间为925ms。主要差距的原因就在于,同步方法等待资源时,就会停止手上的工作,直到有结果返回时,才能继续工作。而异步方法等待资源的同时,还继续处理其他事情。)
二、async/await 特性的结构
同步方法: 如果某一个程序调用某个方法,并在等待方法执行所有处理后才继续执行。
异步方法: 在完成其所有方法之前就返回到调用方法。
特性:
- 调用方法: 该方法调用异步方法,然后在异步方法执行其任务的时候继续执行(可能在相同的线程上,也可能在不同的线程上)。
- 异步方法: 该方法异步执行其工作,然后立即返回到调用方法。
- awaite 表达式: 用于异步方法内部,指明需要异步执行的任务。一个异步方法可以包含任意多个 await 表达式,不过如果一个都不包含的话编译器会发出警告。(大概警告:如果不使用 awaite 表达式,则会视为同步执行。)
三、async/await 特性的整体结构
//调用方法static void Main(){Task value DoAsyncStuff.CalculateSumAsync(5,6);}//异步方法staic class DoAsyncStuff{public static async Task CalculateSumAsync(int i1,int i2){int sum = await TaskEx.Run(() => GetSum(i1,i2));return sum;}}