所谓同步:如果在代码中调用了一个方法,则必须等待该方法所有的代码执行完毕之后,才能回到原来的地方执行下一行代码。
异步:如果不等待调用的方法执行完,就执行下一行代码。
同步例子:
namespace AsyncProgram { class Program { //Calculate the folder's total size private static Int64 CalculateFolderSize(string FolderName) { if (Directory.Exists(FolderName) == false) { throw new DirectoryNotFoundException("文件不存在"); } DirectoryInfo rootDir = new DirectoryInfo(FolderName); //Get all subfolders DirectoryInfo[] childDirs = rootDir.GetDirectories(); //Get all files of current folder FileInfo[] files = rootDir.GetFiles(); Int64 totalSize = 0; //sum every file size foreach (FileInfo file in files) { totalSize += file.Length; } //sum every folder foreach (DirectoryInfo dir in childDirs) { totalSize += CalculateFolderSize(dir.FullName); } return totalSize; } static void Main(string[] args) { Int64 size; String FolderName; Console.WriteLine("Please input the name of folder (C:\\Windows):"); FolderName = Console.ReadLine(); size = CalculateFolderSize(FolderName); Console.WriteLine("\nThe size of folder {0} is {1}字节\n", FolderName, size); Console.ReadKey(); } } }
上面的例子是为了计算文件夹的大小,使用了递归的方法。在Main方法里面是同步调用,下面通过委托改变为异步调用:
//Define a delegate public delegate Int64 CalculateFolderSizeDelegate(String folderName); static void Main(string[] args) { //Int64 size; //String FolderName; //Console.WriteLine("Please input the name of folder (C:\\Windows):"); //FolderName = Console.ReadLine(); //size = CalculateFolderSize(FolderName); //Console.WriteLine("\nThe size of folder {0} is {1}字节\n", FolderName, size); //Console.ReadKey(); CalculateFolderSizeDelegate d = CalculateFolderSize; Console.WriteLine("请输入文件夹的名称"); String folderName = Console.ReadLine(); //Through the async static method CalculateFolderSize IAsyncResult ret = d.BeginInvoke(folderName, null, null); Console.WriteLine("正在计算..."); Int64 size = d.EndInvoke(ret); Console.WriteLine("\n计算完成。文件夹{0}的容量为:{1}字节\n", folderName, size); Console.ReadKey(); }
基于委托的异步编程模式
上面修改之后的代码就是基于委托的异步编程,BeginInvoke是实现异步调用的核心。从这里可以了解到异步编程的一个通用模式:
//.. //普通的代码:处于同步执行模式 d为委托变量 IAsyncResult ret=d.BeginInvoke(......);//启动异步调用 //可以在这一部分干其他的事,程序处于异步执行模式 methodResult=d.EndInvoke(ret);//结束异步调用 //普通的代码:处于同步执行模式
异步编程的基础是委托与多线程,委托编译后会自动生成以下模版:
public sealed class CalculateFolderSizeDelegate:MulticastDelegate { public virtual extern long Invoke(string folderName) {} public virtual extern IAsyncResult BeginInvoke(string folderName, AsyncCallback callback, object @object){} public virtual extern long EndInvoke(IAsyncResult result); }
BeginInvoke方法生成一个IAsyncResult接口类型的对象并填充其对象.借助与IAsyncResult对象,EndInvoke方法不断查询异步调用的方法是否执行完毕,当EndInvoke方法知道异步调用完成时,会取出结果作为返回值。由于EndInvoke方法有一个不断轮询的过程,所以主线程程序直到EndInvoke方法时会暂停等待异步方法调用完成,取回结果后再继续执行。这里我有个问题呢?——EndInvoke方法是怎么知道异步方法调用完成的?如果有大牛路过希望能给出比较详细的答案,或者随着我以后的学习找到了答案在完善博客。
为了是上面的程序更加友好,可以每个1秒输出一个点,改进如下:
static void Main(string[] args) { CalculateFolderSizeDelegate d = CalculateFolderSize; Console.WriteLine("Please input the name of folder (C:\\Windows):"); String folderName = Console.ReadLine(); //Through the async static method CalculateFolderSize IAsyncResult ret = d.BeginInvoke(folderName, null, null); Console.Write("Calculating"); while (ret.IsCompleted==false) { Console.Write("."); System.Threading.Thread.Sleep(1000); } Int64 size = d.EndInvoke(ret); Console.WriteLine("\nThe size of folder {0} is {1}字节\n", folderName, size); Console.ReadKey(); }
还可以使用IAsyncResult提供的AsyncWaitHandler等待句柄对象,它定义了一序列的WaitOne重载方法。在WaitOne通过参数指定的时间段"等待句柄"对象的状态转为Signaled,此时WaitOne方法返回true;如果超出这个时间则返回false,修改如下:
while (!ret.AsyncWaitHandle.WaitOne(1000))
{
Console.Write(".");
}
异步回调
前面都是通过轮询的方式来判断异步是否执行完成,这无疑会在循环等待上浪费不少时间,通过异步回调可以让异步方法执行完成后自动调用一个方法。
首先定义好用于异步调用的委托类型和委托变量,然后定义一个供异步调用的回调方法:
//Define a delegate public delegate Int64 CalculateFolderSizeDelegate(String folderName); private static CalculateFolderSizeDelegate d = CalculateFolderSize; static void Main(string[] args) { String FolderName; while (true) { Console.WriteLine("请输入文件夹名称,输入quit结束程序"); FolderName = Console.ReadLine(); if (FolderName == "quit") { break; } d.BeginInvoke(FolderName, ShowFolderSize, FolderName); } } public static void ShowFolderSize(IAsyncResult result) { Int64 size = d.EndInvoke(result); Console.WriteLine("\n文件夹{0}的容量为:{1}字节\n", (String)result.AsyncState, size); }
注意回调方法的返回值类型是void,只能有一个IAsyncResult类型的参数,并且要在方法体中调用EndInvoke来取回方法执行的结果,另外result参数的AsyncState属性包含了外界传入的参数信息,这里可以看看IAsyncResult类型:
namespace System { // Summary: // Represents the status of an asynchronous operation. [ComVisible(true)] public interface IAsyncResult { // Summary: // Gets a user-defined object that qualifies or contains information about an // asynchronous operation. // // Returns: // A user-defined object that qualifies or contains information about an asynchronous // operation. object AsyncState { get; } // // Summary: // Gets a System.Threading.WaitHandle that is used to wait for an asynchronous // operation to complete. // // Returns: // A System.Threading.WaitHandle that is used to wait for an asynchronous operation // to complete. WaitHandle AsyncWaitHandle { get; } // // Summary: // Gets a value that indicates whether the asynchronous operation completed // synchronously. // // Returns: // true if the asynchronous operation completed synchronously; otherwise, false. bool CompletedSynchronously { get; } // // Summary: // Gets a value that indicates whether the asynchronous operation has completed. // // Returns: // true if the operation is complete; otherwise, false. bool IsCompleted { get; } } }
可以看出这里的AsyncState也是Object类型,与BeginInvoke的第三个参数不谋而合。如果需要传递额外的参数,尽管利用BeginInvoke的第三个参数,这个参数最终会通过IAsyncResult的AsyncState属性传递给回调方法。
异步调用中的异常
在同步执行的方法里面通常处理异常的方式是将可能抛出异常的代码放到try...catch...finally里面,之所以能够捕获到,是因为发生异常的代码与调用的代码位于同一个线程。当调用一个异步方法发生异常时,CLR会捕获并且在EndInvoke方法时再次将异常抛出抛出,所以异步调用中的异常在EndInvoke方法出捕获就行了。
在.net里面有一些组件实现了IAsyncResult异步调用模式,这些组件同时提供了BeginXXX和EndXXX成对的异步方法,并且EndXXX与对应的XXX方法的返回值相同。
基于事件的异步调用模式EAP(Event-based-Asynchronous Pattern)
实现了EAP模式的典型组件是WebClient和BackgroundWorkerEAP模式规定方法名以"Async"结尾的方法是异步调用方法。
WebClient定义了两个同步方法用于Web下载文件:
public void DownloadFile(String address,String fileName);
public void DownloadFile(Uri address,String fileName);
异步方法:
public void DownloadFileAsync(Uri address, string fileName);
public void DownloadFileAsync(Uri address, string fileName, object userToken);
在第二个方法的最后一个参数是为了给每一个异步下载的任务分配一个唯一的标识,这个是程序员的责任。
附一个例子
static void Main(string[] args) { //利用了已经实现了IAsyncResult异步模式的.NET自带的组件WebRequest String UserInputUrl = ""; String FileName = ""; Console.WriteLine("输入URL启动一个异步下载Web文件任务,输入quit退出."); do { Console.Write("\n输入Web文件的URL:"); UserInputUrl = Console.ReadLine(); if (String.IsNullOrWhiteSpace(UserInputUrl)) { Console.WriteLine("不能输入一个空的URL字符串"); continue; } if (UserInputUrl.ToLower() == "quit") { break; } Console.Write("请输入要保存的文件名:"); FileName = Console.ReadLine(); if (String.IsNullOrWhiteSpace(FileName)) { Console.WriteLine("不能输入一个空的URL字符串"); continue; } try { Uri webFileUri = new Uri(UserInputUrl); WebRequest webRequest = WebRequest.Create(webFileUri); DownLoadTask task = new DownLoadTask { SaveToFileName = FileName, WebRequestObject = webRequest }; Console.WriteLine("已在后台自动下载{0}", FileName); webRequest.BeginGetResponse(DownloadFinished, task); } catch (Exception ex) { Console.WriteLine(ex.Message); } } while (true); Console.ReadKey(); } static void DownloadFinished(IAsyncResult ar) { try { DownLoadTask task = ar.AsyncState as DownLoadTask; WebResponse response = task.WebRequestObject.EndGetResponse(ar); String FileContent = ""; using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.GetEncoding("gb2312"))) { FileContent = reader.ReadToEnd(); } using (StreamWriter writer = new StreamWriter(new FileStream(task.SaveToFileName, FileMode.Create), Encoding.GetEncoding("gb2312"))) { writer.Write(FileContent); } MessageBox.Show(String.Format("{0}下载完成!", task.SaveToFileName)); } catch (Exception ex) { throw new Exception(ex.Message); } }