C#秘密武器之异步编程
一、概述
1、什么是异步?
异步操作通常用于执行完成时间可能较长的任务,如打开大文件、连接远程计算机或查询数据库。异步操作在主应用程序线程以外的线程中执行。应用程序调用方法异步执行某个操作时,应用程序可在异步方法执行其任务时继续执行其他的任务。
2、同步与异步的区别
- 同步(Synchronous):在执行某个操作时,应用程序必须等待该操作执行完成后才能继续执行。
- 异步(Asynchronous):在执行某个操作时,应用程序可在异步操作执行时继续执行。实质:异步操作,启动了新的线程,主线程与异步线程(线程池中的线程)并行执行。
3、异步和多线程的区别
- 线程管理:异步线程是由线程池负责管理,而多线程,我们可以自己控制,当然在多线程中我们也可以使用线程池!
- 应用场景:IO密集型工作,采用异步机制;计算密集型工作,采用多线程!
就拿网络扒虫而言,如果使用异步模式去实现,它使用线程池进行管理。异步操作执行时,会将操作丢给线程池中的某个工作线程来完成。当开始I/O操作的时候,异步会将工作线程还给线程池,这意味着获取网页的工作不会再占用任何CPU资源了。直到异步完成,即获取网页完毕,异步才会通过回调的方式通知线程池。
4、为什么要使用异步
- 异步模式借助于线程池,极大地节约了CPU的资源!
- 异步编程不会阻塞主调用线程,有较好的用户体验!
二、使用 IAsyncResult 对象的异步编程
使用IAsyncResult设计模式的异步编程是通过名为 Begin*** 和 End*** 的两个方法来实现的,这两个方法分别指代开始和结束异步操作。
例如,FileStream类提供BeginRead和EndRead方法来从文件异步读取字节,这两个方法实现了 Read 方法的异步版本。
在调用 Begin*** 后,应用程序可以继续在调用线程上执行指令,同时异步操作在另一个线程上执行。(如果有返回值还应调用 End*** 来获取操作的结果)。
Begin*** 方法:
a) Begin*** 方法带有“与该方法的同步版本相同的”参数。
b) Begin*** 方法最后两个参数:
第一个参数定义一个AsyncCallback委托,就是异步操作完成时的回调函数。
public
delegate
void
AsyncCallback(IAsyncResult ar)
第二个参数是用户自定义的一个object类型的对象。可以向回调函数中传递一些信息,在异步操作完成时,在回调函数中可以通过result.AsyncState获取该对象(获取后记得要进行强制类型转换),注意result为IAsyncResult类型!
注意:这两个参数都可以传递null。(但是最好不要这样)
c) Begin*** 方法会返回IAsyncResult对象。
public interface IAsyncResult { // 获取用户定义的对象,它限定或包含关于异步操作的信息。 object AsyncState { get; } // 获取用于等待异步操作完成的System.Threading.WaitHandle,待异步操作完成时获得信号。 WaitHandle AsyncWaitHandle { get; } // 获取一个值,该值指示异步操作是否同步完成。 bool CompletedSynchronously { get; } // 获取一个值,该值指示异步操作是否已完成。 bool IsCompleted { get; } }
End***方法:
a) End*** 方法可结束异步操作,如果调用 End*** 时,IAsyncResult对象表示的异步操作还未完成,则 End*** 将在异步操作完成之前阻塞调用线程。
b) End*** 方法的返回值与其同步副本的返回值类型相同。End*** 方法带有该方法同步版本的签名中声明的所有out 和 ref 参数以及由BeginInvoke返回的IAsyncResult,规范上 IAsyncResult 参数放最后。
注意:1、要想获得返回结果,必须调用的方法;
2、若带有out 和 ref 参数,实现上委托也要带有out 和 ref 参数,以便在回调中获得对应引用传参值做相应逻辑;
3、总是调用 End***() 方法,而且只调用一次
AsyncCallback回调:
a) 异步调用的异常在此处扑捉,也就是try catch放在回调函数里边
b) 异步调用时的同步问题(对于共享资源的访问)也在该处处理
.NET中已经实现了许多IAsyncResult异步调用模式的组件,注意使用!
下面来看用该模式实现异步调用的一个栗子:
#region use APM to download file asynchronously private static void DownloadFileAsync(string url) { try { // Initialize an HttpWebRequest object HttpWebRequest myHttpWebRequest = (HttpWebRequest)WebRequest.Create(url); // Create an instance of the RequestState and assign HttpWebRequest instance to its request field. RequestState requestState = new RequestState(); requestState.request = myHttpWebRequest; myHttpWebRequest.BeginGetResponse(new AsyncCallback(ResponseCallback), requestState); } catch (Exception e) { Console.WriteLine("Error Message is:{0}",e.Message); } } // The following method is called when each asynchronous operation completes. private static void ResponseCallback(IAsyncResult callbackresult) { // Get RequestState object RequestState myRequestState = (RequestState)callbackresult.AsyncState; HttpWebRequest myHttpRequest = myRequestState.request; // End an Asynchronous request to the Internet resource myRequestState.response = (HttpWebResponse)myHttpRequest.EndGetResponse(callbackresult); // Get Response Stream from Server Stream responseStream = myRequestState.response.GetResponseStream(); myRequestState.streamResponse = responseStream; IAsyncResult asynchronousRead = responseStream.BeginRead(myRequestState.BufferRead, 0, myRequestState.BufferRead.Length, ReadCallBack, myRequestState); } // Write bytes to FileStream private static void ReadCallBack(IAsyncResult asyncResult) { try { // Get RequestState object RequestState myRequestState = (RequestState)asyncResult.AsyncState; // Get Response Stream from Server Stream responserStream = myRequestState.streamResponse; // int readSize = responserStream.EndRead(asyncResult); if (readSize > 0) { myRequestState.filestream.Write(myRequestState.BufferRead, 0, readSize); responserStream.BeginRead(myRequestState.BufferRead, 0, myRequestState.BufferRead.Length, ReadCallBack, myRequestState); } else { Console.WriteLine("\nThe Length of the File is: {0}", myRequestState.filestream.Length); Console.WriteLine("DownLoad Completely, Download path is: {0}", myRequestState.savepath); myRequestState.response.Close(); myRequestState.filestream.Close(); } } catch (Exception e) { Console.WriteLine("Error Message is:{0}", e.Message); } } #endregion
运行结果为(从运行结果也可以看出,在主线程中调用 DownloadFileAsync(downUrl)方法时,DownloadFileAsync(downUrl)方法中的myHttpWebRequest.BeginGetResponse调用被没有阻塞调用线程(即主线程),而是立即返回到主线程,是主线程后面的代码可以立即执行)
三、基于委托的异步编程模式
对于委托,编译器会为我们生成异步调用方法“BeginInvoke”和“EndInvoke”。异步委托是快速为方法构建异步调用的方式,它是基于IAsyncResult设计模式实现的异步调用:通过BeginInvoke返回IAsyncResult对象;通过EndInvoke获取结果值。
原理与IAsyncResult模式一致!
using System; using System.Text; namespace AsynSample { /// <summary> /// 下载委托 /// </summary> /// <param name="fileName"></param> public delegate string AysnDownloadDelegate(string fileName); /// <summary> /// 通过委托实现异步调用 /// </summary> class DownloadFile { /// <summary> /// 同步下载 /// </summary> /// <param name="fileName"></param> public string Downloading(string fileName) { string filestr = string.Empty; Console.WriteLine("下载事件开始执行"); System.Threading.Thread.Sleep(3000); Random rand = new Random(); StringBuilder builder =new StringBuilder(); int num; for(int i=0;i<100;i++) { num = rand.Next(1000); builder.Append(i); } filestr = builder.ToString(); Console.WriteLine("下载事件执行结束"); return filestr; } /// <summary> /// 异步下载 /// </summary> public IAsyncResult BeginDownloading(string fileName) { string fileStr = string.Empty; AysnDownloadDelegate downloadDelegate = new AysnDownloadDelegate(Downloading); return downloadDelegate.BeginInvoke(fileName, Downloaded, downloadDelegate); } /// <summary> /// 异步下载完成后事件 /// </summary> /// <param name="result"></param> private void Downloaded(IAsyncResult result) { AysnDownloadDelegate aysnDelegate = result.AsyncState as AysnDownloadDelegate; if (aysnDelegate != null) { string fileStr = aysnDelegate.EndInvoke(result); if (!string.IsNullOrEmpty(fileStr)) { Console.WriteLine("下载文件:{0}", fileStr); } else { Console.WriteLine("下载数据为空!"); } } else { Console.WriteLine("下载数据为空!"); } } } }
四、基于事件的异步编程模式
简称EAP,从.NET 2.0起就提出了该异步编程模式,EAP主要是为Windows窗体开发人员创建的(C/S模式)。
实现了基于事件的异步模式的类将具有一个或者多个以Async为后缀的方法和对应的Completed事件,并且这些类都支持异步方法的取消、进度报告和报告结果。 在.NET 类库中只有部分的类支持EAP的(共17个类):
System.Object的派生类型:
System.Activies.WorkflowInvoke
System.Deployment.Application.ApplicationDeployment
System.Deployment.Application.InPlaceHosingManager
System.Net.Mail.SmtpClient
System.Net.PeerToPeer.PeerNameResolver
System.Net.PeerToPeer.Collaboration.ContactManager
System.Net.PeerToPeer.Collaboration.Peer
System.Net.PeerToPeer.Collaboration.PeerContact
System.Net.PeerToPeer.Collaboration.PeerNearMe
System.ServiceModel.Activities.WorkflowControlClient
System.ServiceModel.Discovery.AnnoucementClient
System.ServiceModel.Discovery.DiscoveryClient
System.ComponentModel.Component的派生类型:
System.ComponentModel.BackgroundWorker
System.Media.SoundPlay
System.Net.WebClient
System.Net.NetworkInformation.Ping
System.Windows.Forms.PictureBox(继承于Control类,Control类派生于Component类)
举个栗子:
class Program { static void Main(string[] args) { WebClient client = new WebClient(); client.DownloadFileCompleted += new System.ComponentModel.AsyncCompletedEventHandler(client_DownloadFileCompleted); client.DownloadFileAsync(new Uri("http://imgsrc.baidu.com/baike/abpic/item/6a600c338744ebf844a0bc74d9f9d72a6159a7ac.jpg"), "1.jpg", "图片下完了,你懂的!"); Console.WriteLine("我是主线程,我不会被阻塞!"); Console.Read(); } static void client_DownloadFileCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e) { Console.WriteLine("\n" + e.UserState); } }
五、基于Task的异步编程模式
上面的集中异步编程模式在一定程度上是比较繁琐的,那么问题来了,异步编程哪家强?在.NET 4.0中提出了一个新的异步模式——基于任务的异步模式(TAP),该模式主要使用System.Threading.Tasks.Task和Task<T>类来完成异步编程,相对于前面几种异步模式来讲,TAP使异步编程模式更加简单(因为这里我们只需要关注Task这个类的使用),同时TAP也是微软推荐使用的异步编程模式。
.NET的类中“存在TaskAsync为后缀的方法”时就代表该类实现了TAP, 并且基于任务的异步模式同样也支持异步操作的取消和进度的报告的功能~
Task
.NET 4.0推出了新一代的多线程模型Task。这里将以一个简单的Demo来看一下Task的使用,同时与Thread的创建方式做一下对比。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; namespace TestApp { class Program { static void Main(string[] args) { Console.WriteLine("主线程启动"); //.NET 4.5引入了Task.Run静态方法来启动一个线程 Task.Run(() => { Thread.Sleep(1000); Console.WriteLine("Task1启动"); }); //Task启动的是后台线程,假如要在主线程中等待后台线程执行完毕,可以调用Wait方法 Task task = Task.Run(() => { Thread.Sleep(500); Console.WriteLine("Task2启动"); }); task.Wait(); Console.WriteLine("主线程结束"); } } }
首先,必须明确一点是Task启动的线程是后台线程,不过可以通过在Main方法中调用task.Wait()方法,使应用程序等待task执行完毕。Task与Thread的一个重要区分点是:Task底层是使用线程池的,而Thread每次实例化都会创建一个新的线程。这里可以通过这段代码做一次验证:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; namespace TestApp { class Program { static void DoRun1() { Console.WriteLine("Thread Id =" + Thread.CurrentThread.ManagedThreadId); } static void DoRun2() { Thread.Sleep(50); Console.WriteLine("Task调用Thread Id =" + Thread.CurrentThread.ManagedThreadId); } static void Main(string[] args) { for (int i = 0; i < 50; i++) { new Thread(DoRun1).Start(); } for (int i = 0; i < 50; i++) { Task.Run(() => { DoRun2(); }); } //让应用程序不立即退出 Console.Read(); } } }
运行代码,可以看到DoRun1()方法每次的Thread Id都是不同的,而DoRun2()方法的Thread Id是重复出现的。我们知道线程的创建和销毁是一个开销比较大的操作,Task.Run()每次执行将不会立即创建一个新线程,而是到CLR线程池查看是否有空闲的线程,有的话就取一个线程处理这个请求,处理完请求后再把线程放回线程池,这个线程也不会立即撤销,而是设置为空闲状态,可供线程池再次调度,从而减少开销。
Task<TResult>
Task<TResult>是Task的泛型版本,这两个之间的最大不同是Task<TResult>可以有一个返回值,看一下代码应该一目了然:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; namespace TestApp { class Program { static void Main(string[] args) { Console.WriteLine("主线程开始"); Task<string> task = Task<string>.Run(() => { Thread.Sleep(1000); return Thread.CurrentThread.ManagedThreadId.ToString(); }); Console.WriteLine(task.Result); Console.WriteLine("主线程结束"); } } }
Task<TResult>的实例对象有一个Result属性,当在Main方法中调用task.Result的时候,将等待task执行完毕并得到返回值,这里的效果跟调用task.Wait()是一样的,只是多了一个返回值。
async/await 特性
.NET 4.5 的推出,对于C#又有了新特性的增加——就是C#5.0中async和await两个关键字,这两个关键字简化了异步编程,之所以简化了,是因为编译器给我们做了更多的工作,async/await特性是与Task紧密相关的。先通过代码来感受一下这两个特性的使用。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; namespace TestApp { class Program { static void Main(string[] args) { Console.WriteLine("-------主线程启动-------"); Task<int> task = GetLengthAsync(); Console.WriteLine("Main方法做其他事情"); Console.WriteLine("Task返回的值" + task.Result); Console.WriteLine("-------主线程结束-------"); } static async Task<int> GetLengthAsync() { Console.WriteLine("GetLengthAsync Start"); string str = await GetStringAsync(); Console.WriteLine("GetLengthAsync End"); return str.Length; } static Task<string> GetStringAsync() { return Task<string>.Run(() => { Thread.Sleep(2000); return "finished"; }); } } }
首先来看一下async关键字。async用来修饰方法,表明这个方法是异步的,声明的方法的返回类型必须为:void或Task或Task<TResult>。返回类型为Task的异步方法中无需使用return返回值,而返回类型为Task<TResult>的异步方法中必须使用return返回一个TResult的值,如上述Demo中的异步方法返回一个int。
再来看一下await关键字。await必须用来修饰Task或Task<TResult>,而且只能出现在已经用async关键字修饰的异步方法中。
通常情况下,async/await必须成对出现才有意义,假如一个方法声明为async,但却没有使用await关键字,则这个方法在执行的时候就被当作同步方法,这时编译器也会抛出警告提示async修饰的方法中没有使用await,将被作为同步方法使用。了解了关键字async\await的特点后,我们来看一下上述Demo在控制台会输出什么吧。
输出的结果已经很明确地告诉我们整个执行流程了。GetLengthAsync异步方法刚开始是同步执行的,所以"GetLengthAsync Start"字符串会被打印出来,直到遇到第一个await关键字,真正的异步任务GetStringAsync开始执行,await相当于起到一个标记/唤醒点的作用,同时将控制权放回给Main方法,"Main方法做其他事情"字符串会被打印出来。之后由于Main方法需要访问到task.Result,所以就会等待异步方法GetLengthAsync的执行,而GetLengthAsync又等待GetStringAsync的执行,一旦GetStringAsync执行完毕,就会回到await GetStringAsync这个点上执行往下执行,这时"GetLengthAsync End"字符串就会被打印出来。
当然,我们也可以使用下面的方法完成上面控制台的输出。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; namespace TestApp { class Program { static void Main(string[] args) { Console.WriteLine("-------主线程启动-------"); Task<int> task = GetLengthAsync(); Console.WriteLine("Main方法做其他事情"); Console.WriteLine("Task返回的值" + task.Result); Console.WriteLine("-------主线程结束-------"); } static Task<int> GetLengthAsync() { Console.WriteLine("GetLengthAsync Start"); Task<int> task = Task<int>.Run(() => { string str = GetStringAsync().Result; Console.WriteLine("GetLengthAsync End"); return str.Length; }); return task; } static Task<string> GetStringAsync() { return Task<string>.Run(() => { Thread.Sleep(2000); return "finished"; }); } } } //不使用async\await
对比两种方法,是不是async\await关键字的原理其实就是通过使用一个线程完成异步调用吗?答案是否定的。async关键字表明可以在方法内部使用await关键字,方法在执行到await前都是同步执行的,运行到await处就会挂起,并返回到Main方法中,直到await标记的Task执行完毕,才唤醒回到await点上,继续向下执行。
async/await 实际应用
微软已经对一些基础类库的方法提供了异步实现,接下来将实现一个例子来介绍一下async/await的实际应用。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web; using System.Threading; using System.Threading.Tasks; using System.Net; namespace TestApp { class Program { static void Main(string[] args) { Console.WriteLine("开始获取博客园首页字符数量"); Task<int> task1 = CountCharsAsync("http://www.cnblogs.com"); Console.WriteLine("开始获取百度首页字符数量"); Task<int> task2 = CountCharsAsync("http://www.baidu.com"); Console.WriteLine("Main方法中做其他事情"); Console.WriteLine("博客园:" + task1.Result); Console.WriteLine("百度:" + task2.Result); } static async Task<int> CountCharsAsync(string url) { WebClient wc = new WebClient(); string result = await wc.DownloadStringTaskAsync(new Uri(url)); return result.Length; } } } //Demo