一、什么是异步
启动程序时,系统会在内存中创建一个新的进程。
进程: 构成运行程序的资源的集合。这些资源包括虚地址空间、文件句柄和程序运行所需的其他许多东西。
在进程内部,系统创建了一个称为线程的内核对象,它代表了真正执行的程序。(线程是“执行线程”的简称。)一旦进程建立,系统会在 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<int> t1 = CountCharacters(1, "https://www.baidu.com/");
Task<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.Result}");//此处代码修改
Console.WriteLine($"Chars in http://www.illustractedcsharp.com :{t2.Result}");//此处代码修改
}
//获取网页源代码内容的长度,此处代码修改
private async Task<int> 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<int> 类型的,所以这两行代码需要等待 DownloadStringTaskAsync执行完毕后,返回其值才能被输出。如果该方法没有执行完毕,则阻塞并等待期值到来。
以上例子使用了异步调用,但是没有开辟新线程。是因为程序主线程异步调用了 DownloadStringTaskAsync 告诉系统它想要获取资源后,然后继续做自己的事情了。如果中间过程中,在后台系统请求网络得出结果后,会告诉主线程并把结果返回给它,它就停止手上的工作继续处理这个事情,处理完后再继续执行之前手上的工作。
由以上两个示例的输出结果可知,如果方法里有需要等待的代码块,那么该方法为同步方法时就会比作为异步方法时,需要耗费的时间就比较长。
(不使用异步示例中,该方法执行的整个过程所耗费的时间为3,323ms;而使用异步后,需要时间为925ms。主要差距的原因就在于,同步方法等待资源时,就会停止手上的工作,直到有结果返回时,才能继续工作。而异步方法等待资源的同时,还继续处理其他事情。)
二、async/await 特性的结构
同步方法: 如果某一个程序调用某个方法,并在等待方法执行所有处理后才继续执行。
异步方法: 在完成其所有方法之前就返回到调用方法。
特性:
- 调用方法: 该方法调用异步方法,然后在异步方法执行其任务的时候继续执行(可能在相同的线程上,也可能在不同的线程上)。
- 异步方法: 该方法异步执行其工作,然后立即返回到调用方法。
- awaite 表达式: 用于异步方法内部,指明需要异步执行的任务。一个异步方法可以包含任意多个 await 表达式,不过如果一个都不包含的话编译器会发出警告。(大概警告:如果不使用 awaite 表达式,则会视为同步执行。)
三、async/await 特性的整体结构
//调用方法
static void Main()
{
Task<int> value DoAsyncStuff.CalculateSumAsync(5,6);
}
//异步方法
staic class DoAsyncStuff
{
public static async Task<int> CalculateSumAsync(int i1,int i2)
{
int sum = await TaskEx.Run(() => GetSum(i1,i2));
return sum;
}
}