异步模式

一、    异步概述

  1. 1.   进程和线程

程序在启动时,系统会在内存中创建一个进程。进程是程序运行所需资源的集合,这些资源包括虚地址空间、文件句柄和其他程序运行所需的东西。在进程的内部,系统创建一个称为线程的内核对象,代表真正执行的程序。当线程被建立时,系统在Main方法的第一行语句处开始执行线程。

关于线程

l  默认情况,一个进程只包含一个线程,从程序的开始到执行结束。

l  线程可以派生自其它线程,所以一个进程可以包含不同状态的多个线程,来执行程序的不同部分。

l  一个进程中的多个线程,将共享该进程的资源。

l  系统为处理器执行所规划的单元是线程,而非进程

 

  1. 2.   什么是同步和异步

同步(Synchronous):在执行某个操作时,应用程序必须等待该操作执行完成后才能继续执行

异步(Asynchronous):在执行某个操作时,应用程序可在异步操作执行时继续执行其他操作。异步操作的实质是启动了新的线程,主线程与方法线程并行执行。

 

  1. 3.   异步的重要性

当某个操作需要花费大量的时间进行处理,若是使用同步编程,那么程序在等待响应的时间内不能处理其他事物,这样不仅效率比较低,也经常会导致界面停止响应或者IIS线程占用过多等问题;而使用异步编程时,在进行等待相应的时间内,程序可以利用等待的时间处理其他事物,当得到响应时,再回到响应处继续执行,这样程序的效率会更高。

有时应用程序要求立刻响应用户的请求,否则用户就会不断重复同一个动作,导致延迟的情况越来越严重。如Visual Studio 2010就是经常阻塞UI线程的应用程序之一。Visual Studio 2012之后,情况就不一样了,因为项目都是在后台异步加载的。

 

  1. 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. 1.  TAP异步模式

基于任务的异步模式中,核心的类是Task或者Task<TResult>,该类就表示一个异步操作。

以WebClient类来说,在.NET 4.5中更新了这个类,使其可以支持基于任务模式的异步操作。在该模式下定义了带有Async后缀的方法,并返回一个Task类型。

 

  1. 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关键字会解除线程(主线程)的阻塞,完成其他任务。

 

  1. 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,作为我们异步方法的返回值

 

  1. 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();