异步模式
一、 异步概述
- 1. 进程和线程
程序在启动时,系统会在内存中创建一个进程。进程是程序运行所需资源的集合,这些资源包括虚地址空间、文件句柄和其他程序运行所需的东西。在进程的内部,系统创建一个称为线程的内核对象,代表真正执行的程序。当线程被建立时,系统在Main方法的第一行语句处开始执行线程。
关于线程
l 默认情况,一个进程只包含一个线程,从程序的开始到执行结束。
l 线程可以派生自其它线程,所以一个进程可以包含不同状态的多个线程,来执行程序的不同部分。
l 一个进程中的多个线程,将共享该进程的资源。
l 系统为处理器执行所规划的单元是线程,而非进程
- 2. 什么是同步和异步
同步(Synchronous):在执行某个操作时,应用程序必须等待该操作执行完成后才能继续执行
异步(Asynchronous):在执行某个操作时,应用程序可在异步操作执行时继续执行其他操作。异步操作的实质是启动了新的线程,主线程与方法线程并行执行。
- 3. 异步的重要性
当某个操作需要花费大量的时间进行处理,若是使用同步编程,那么程序在等待响应的时间内不能处理其他事物,这样不仅效率比较低,也经常会导致界面停止响应或者IIS线程占用过多等问题;而使用异步编程时,在进行等待相应的时间内,程序可以利用等待的时间处理其他事物,当得到响应时,再回到响应处继续执行,这样程序的效率会更高。
有时应用程序要求立刻响应用户的请求,否则用户就会不断重复同一个动作,导致延迟的情况越来越严重。如Visual Studio 2010就是经常阻塞UI线程的应用程序之一。Visual Studio 2012之后,情况就不一样了,因为项目都是在后台异步加载的。
- 4. 异步的使用模式
使用异步模式编程主要有以下三种模式:
1) 异步编程模型 (APM) 模式(也称为 IAsyncResult 模式),其中异步操作要求 Begin 和 End 方法。
2) 基于事件的异步模式 (EAP) 需要一个具有 Async 后缀的方法,还需要一个或多个事件、事件处理程序、委托类型和 EventArg 派生的类型。
3) 基于任务的异步模式 (TAP),该模式使用一个方法表示异步操作的启动和完成。
二、 异步编程模型 (APM)模式
APM模式的异步编程模型也称为IasyncResult模式。通过带有Begin和End前缀两个方法名来实现,如:FileStream类提供 BeginRead 和 EndRead 方法来从文件异步读取字节,这两个方法实现了Read方法的异步版本;还有异步写操作的 BeginWrite 和 EndWrite等。示例如下:
/// <summary>
/// 异步读取文件
/// </summary>
/// <param name="path">文件路径</param>
public void AsyncReadFile(string path)
{
Console.WriteLine("异步读取文件开始");
if (File.Exists(path))
{
FileStream fs = new FileStream(path, FileMode.Open);
BufferSize = fs.Length;
Buffer = new byte[BufferSize];
//调用FileStream类中异步读取文件的方法BeginRead
fs.BeginRead(Buffer, 0, (int)BufferSize, EndCallBack, fs);
//注意:此处不能关闭流对象,因为读取过程还没结束
}
else
{
Console.WriteLine("文件不存在");
}
}
在调用 Begin前缀的方法后,应用程序可以继续在调用线程上执行指令,同时异步操作在另一个线程上执行。每次调用Begin前缀的方法时,应用程序还应调用End前缀的方法来获取操作的结果。Begin前缀的方法开始异步操作并返回一个实现 IAsyncResult 接口的对象。
IAsyncResult 对象存储有关异步操作的信息
名 称 | 说 明 |
AsyncState | 获取用户定义的对象,它限定或包含关于异步操作的信息 |
AsyncWaitHandle | 获取用于等待异步操作完成的 WaitHandle |
CompletedSynchronously | 获取一个值,该值指示异步操作是否同步完成 |
IsCompleted | 获取一个值,该值指示异步操作是否已完 |
/// <summary>
/// 异步开始读取文件的回调方法
/// </summary>
/// <param name="ar">传递有关异步操作的相关信息</param>
void EndCallBack(IAsyncResult ar)
{
//AsyncState属性获取异步操作中,用户定义的对象,然后转为文件流对象
FileStream fs = ar.AsyncState as FileStream;
if (fs!=null)
{
//结束读取
fs.EndRead(ar);
//关闭文件流释放资源
fs.Close();
string content = Encoding.UTF8.GetString(Buffer);
Console.WriteLine("读取的文件信息:{0}", content);
Console.WriteLine("异步读取结束");
}
}
如果是同步读取,在读取时,当前线程读取文件,只能等到读取完毕,才能执行后续的操作。
而异步读取,是创建了新的线程,读取文件,而主线程,则继续执行自己的任务,那么这样新的线程在读取文件时就不会影响主线程部分的响应。
三、 基于事件的异步模式(EPA)
EPA基于事件的异步模式是.NET 2.0提出来的,实现了基于事件的异步模式的类将具有一个或者多个以Async为后缀的方法和对应的Completed事件,然而.NET中并不是所有的类都支持EPA这种基于事件的异步处理模式。
当调用基于事件的EPA模式的类的XXXAsync方法时,就开始了一个异步操作,该方法调用完成后会触发相应的xxxCompelted事件通知线程池的线程去执行耗时的操作,所以当主线程调用该方法时,就不会阻塞主线程了。
位于System.NET命名空间下的WebClient类就是一个典型的支持基于事件异步模式操作的类。WebClient提供用于将数据发送到和接收来自通过 URI 确认的资源数据的方法。
例:
/// <summary>
/// 使用EPA模式异步读取页面源代码
/// </summary>
/// <param name="uri">统一资源定位符</param>
public void AsyncReadWeb(string uri)
{
//实例化一个WebClient对象
WebClient client = new WebClient();
//设置下载字符串的编码方式
client.Encoding = Encoding.UTF8;
Console.WriteLine("开始异步读取");
//调用异步读取页面源文件的方法
client.DownloadStringAsync(new Uri(uri));
//为WebClient对象的相应异步方法完成后的事件订阅一个处理程序
client.DownloadStringCompleted += Client_DownloadStringCompleted;
}
/// <summary>
/// DownloadStringCompleted事件处理函数
/// </summary>
/// <param name="sender">事件源</param>
/// <param name="e">包含事件数据</param>
void Client_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
//使用事件参数e可以获取异步读取的数据
Console.WriteLine(e.Result);
}
基于事件的EAP模式是基于APM模式之上的,而APM又是建立在委托之上的
四、 基于任务的异步模式(TAP)
- 1. TAP异步模式
基于任务的异步模式中,核心的类是Task或者Task<TResult>,该类就表示一个异步操作。
以WebClient类来说,在.NET 4.5中更新了这个类,使其可以支持基于任务模式的异步操作。在该模式下定义了带有Async后缀的方法,并返回一个Task类型。
- 2. async和await关键字
1) 使用async关键字可将方法、lambda表达式或匿名方法标记为异步,即方法中应该包含一个或多个await表达式,但async关键字本身不会创建异步操作。
例:
public async Task methodAsync( )
{
//doing something…
须使用await关键字,表示等待异步操作
}
2) 使用async和await定义一个异步方法需要注意以下几点
l 使用async关键字来修饰方法。
l 在异步方法中使用await关键字(不使用编译器会给出警告但不报错),否则异步方法会以同步方式执行。
l 异步方法名称以Async结尾。
l 异步方法中不能声明使用ref或out关键字修饰的变量。
l 不要将程序入口点(Main方法)声明为async,也不能在其中使用await关键字
例:使用基于任务模式的异步处理载入页面源代码
/// <summary>
/// 1.使用基于任务模式的异步处理载入页面源代码
/// </summary>
/// <param name="url"></param>
static async void GetWebString(string url)
{
WebClient wc = new WebClient();
wc.Encoding = Encoding.UTF8;
//调用了wc对象的DownloadStringTaskAsync()异步下载方法
//await关键字,表示等待异步下载操作
string source = await wc.DownloadStringTaskAsync(url);
//调用时不使用await关键字,该方法会获取一个Task<string>类型的任务对象
//下面写法也可以实现异步下载
//Task<string> task = wc.DownloadStringTaskAsync(url);
//string source = await task;
Console.WriteLine(source);
}
DownloadStringTaskAsync方法声明为返回Task<string>。但是,不需要声明一个Task<string>类型的变量来获取DownloadStringTaskAsync方法返回的结果。只要声明一个string类型的变量,并使用await关键字。await关键字会解除线程(主线程)的阻塞,完成其他任务。
- 3. 异步方法的返回值
1) 在定义方法时,添加 async 关键字后,需要返回一个将用于后续操作的对象,就使用 Task<TResult>。其中包含一个指定类型TResult。
例:创建一个名为GetDateTimeAsync的异步方法来获取当前系统时间
static async Task<DateTime> GetDateTimeAsync()
{
//Task.FromResult是一个占位符,此处是DateTime类型
return await Task.FromResult(DateTime.Now);
}
创建另一个异步方法来调用GetDateTimeAsync方法
static async void CallAsync()
{
Console.WriteLine("异步开始");
//在另一个异步方法中调用异步方法的方式
DateTime now1 = await GetDateTimeAsync();
//另一种调用方式
Task<DateTime> t = GetDateTimeAsync();
DateTime now2 = await t;
//使异步操作延迟1秒
await Task.Delay(1000);
//输出的结果对比
Console.WriteLine("当前时间:"+now1);
Console.WriteLine("当前时间:"+now2);
Console.WriteLine("t.Result:" + t.Result);
Console.WriteLine("异步结束");
}
2) Task
一个返回类型为Task类型的异步方法,他的具体实现不包含return语句,或者说是一个 return void 的语句。这个Task类型是不包含属性Result的。跟 Task<TResult> 调用一样,调用方法直接使用await挂起并等待异步方法的执行完毕。
例:
async Task DelayAsync()
{
//Task.Delay 是一个占位符,用于假设方法正处于工作状态。
await Task.Delay(100);
Console.WriteLine("OK!");
}
通过使用 await 语句调用和等待 DelayAsync方法,类似于返回 void 的方法的调用语句。 await 运算符的应用程序在这种情况下不生成值。
例:await DelayAsync();
也可以将调用和等待的语句分开
例:Task delayTask = DelayAsync();
await delayTask;
3) void
void 返回类型主要用在事件处理程序中,一种称为“fire and forget”(触发并忘记)的活动的方法。
除了它之外,我们都应该尽可能是用 Task,作为我们异步方法的返回值
- 4. 异步编程中的异常处理
1) 定义一个方法,在延迟后抛出一个异常
/// <summary>
/// 抛出异常的异步方法
/// </summary>
/// <param name="timeout">延迟的时长</param>
/// <param name="message">自定义的异常消息</param>
/// <returns></returns>
static async Task ThrowAfter(int timeout, string message)
{
await Task.Delay(timeout);
throw new Exception(message);
}
2) 在Main方法中调用该方法,并使用Try…Catch…语句尝试对异常进行捕获
//没有捕获到异常的原因是因为Main方法在ThrowAfter抛出异常之前就已经执行完毕了
try
{
Console.WriteLine("主线程开始执行");
ThrowAfter(1000, "我是一个异步方法执行时产生的异常");
Console.WriteLine("主线程执行结束");
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
从同步编程的角度来看,try块中的语句如果在执行过程中出现异常,catch块将会捕捉该异常。但是此处ThrowAfter是一个异步方法,在Main方法中没有捕获到异常。原因是Main方法在ThrowAfter抛出异常之前就已经执行完毕了。
3) 异步方法的异常处理比较好的处理方式,就是使用await关键字,将其放在try…catch…语句中,但是Main方法不能标识为async。此时可以再创建一个方法来调用ThrowAfter方法。
/// <summary>
/// 捕获异步方法的异常
/// </summary>
static async void CatchOneError()
{
try
{
//在try语句块中使用await关键字,处理异常
await ThrowAfter(1000, "我是一个异步方法执行时产生的异常");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
4) 再改写Main方法中的代码
Console.WriteLine("主线程开始执行");
CatchOneError();
Console.WriteLine("主线程执行结束");
Console.ReadLine();