第八章 异步编程模式

目录

1 异步编程的必要性

1.1 同步调用与异步调用

1.2 基于委托的异步调用

1.3 等待异步调用完成

1.4 处理异步调用时的异常

1.5 异步编程小结


1 异步编程的必要性

.NET 的异步编程技术主要分为两大块:“使用 IAsyncResult 的异步编程模式” 和 “基于事件的异步编程模式”

1.1 同步调用与异步调用

通常情况下,当我们调用方法 A 后,在 A 返回之前,调用线程得不到程序执行的控制权,也就是说,方法 A 后面的代码是不可能执行的,直到 A 返回为止,这种调用方式称之为 “同步调用”;相反,如果调用在返回之前,调用线程依旧保留控制权,能够继续执行后面的代码,那么称这种调用方式为 “异步调用”,如下图所示。

第八章 异步编程模式_第1张图片

如上图所示,左边为同步调用,调用线程被阻塞,右边为异步调用,调用线程没有被阻塞。

异步调用的优点

很明显,异步调用不会阻塞调用线程,单位时间内可以处理更多的任务。在 Winform 中异步调用的作用尤其明显。如果我们在 UI 线程中同步调用一个耗时方法,那么窗体界面会出现反应迟钝、卡以及不刷新等现象,主要原因就是同步调用的方法阻塞了 UI 线程,Windows消息不能及时被处理。

异步调用过程中,方法的执行不在调用线程中。也就是说,真正执行任务的线程不再是当前调用线程,那么就会有以下几个问题:

(1)当前调用线程如何知道任务什么时候执行结束,任务执行的结果如何?

(2)当前调用线程怎样给执行任务的线程提供一些额外的参数?

(3)当前调用线程怎样捕获执行任务时可能抛出的异常?

1.2 基于委托的异步调用

委托的一个作用就是可以异步调用与它关联的方法,我们即可以选择同步调用一个委托,也可以选择异步调用一个委托。理论上讲,任何一个方法,通过委托包装后,都可以实现异步调用。

1.2.1 委托的 BeginInvoke 与 EndInvoke 方法

.NET 编译器为我们定义的每个委托类型自动生成了两个方法:BeginInvoke 和 EndInvoke,这两个方法专门用来负责异步调用委托。

BeginInvoke 方法签名如下:

public IAsyncResult BeginInvoke(<输入和输出变量>,AsyncCallback callback,object asyncState);

第一个参数 是定义委托时确定的方法签名中的参数列表;

第二个参数 callback 是当异步调用结束时自动回调的方法;

public delegate void AsyncCallback(IAsyncResult ar);

第三个参数 asyncState 用于向第二个参数所确定的 callback 回调方法提供参数;

BeginInvoke 方法返回一个 IAsyncResult 接口类型:

public interface IAsyncResult
{  
   object AsyncState { get; }
   WaitHandle AsyncWaitHandle { get; }

   // 判断异步操作是否同步完成
   bool CompletedSynchronously { get; }
   
   // 判断异步操作是否已完成
   bool IsCompleted { get; }
}

EndInvoke 方法签名如下:

public <方法返回值类型> EndInvoke(IAsyncResult result);

借助于 IAsyncResult 对象,EndInvoke 方法不断查询异步调用的方法是否执行完毕。当EndInvoke 方法发现异步调用完成时,它取出此异步调用方法执行的结果作为其返回值。这样,启动异步调用的调用者线程就可以获取异步调用方法的结果了。

BeginInvoke 一执行就马上返回,不会阻塞调用线程。而由于 EndInvoke 方法有一个不断轮询的过程,所以主线程程序会在执行到调用 EndInvoke 方法时阻塞,等待异步调用方法完成,取回结果后再继续运行。

第八章 异步编程模式_第2张图片

图中白色部分代表线程拥有控制权,虚线阴影部分代表线程失去控制权。调用 BeginInvoke 后,调用线程还拥有控制权,调用 EndInvoke 后,如果异步调用还未结束,那么当前线程失去控制权,也就是 EndInvoke 会阻塞当前调用线程(图中正是这种情况),但是如果异步调用已经结束,那么 EndInvoke 会马上返回,当前调用线程依旧保留控制权,即此时 EndInvoke 不会阻塞当前调用线程。

class Program
{
   static void Main(string[] args)
   {
       MyClass m_obj = new MyClass();
       Func del = m_obj.CombineString;
       string result = del("Demo", "Test");
       Console.WriteLine(result); // 输出 result 需要3秒
       IAsyncResult ar1 = del.BeginInvoke("Hello", "World", null, null);
       IAsyncResult ar2 = del.BeginInvoke("C#", ".NET", null, null);
       string result1 = del.EndInvoke(ar1);
       string result2 = del.EndInvoke(ar2);
       Console.WriteLine(result1);
       Console.WriteLine(result2);
   }
}

class MyClass
{
   public string CombineString(string first, string second)
   {
       Thread.Sleep(5000);
       return first + "-" + second;
   }
}

注:如果我们使用同步调用的话,输出 result1 与 result2 总耗时为 10 秒;但是如果采用如上的示例代码,总耗时为 5 秒。

1.3 等待异步调用完成

1.3.1 使用轮询

IAsyncResult 接口中有一个 IsCompleted 字段,可用于检查异步调用是否完成:

static void Main(string[] args)
{
   Func del = m_obj.CombineString;
   IAsyncResult ar1 = del.BeginInvoke("Hello", "World", null, null);
   IAsyncResult ar2 = del.BeginInvoke("C#", ".NET", null, null);
   Console.Write("正在计算,请耐心等待");
   while ((ar1.IsCompleted && ar2.IsCompleted) == false)
   {
       Console.Write(".");
       Thread.Sleep(1000);
   }
   Console.WriteLine();
   string result1 = del.EndInvoke(ar1); // 调用完成,取出结果
   string result2 = del.EndInvoke(ar2);
   Console.WriteLine(result1);
   Console.WriteLine(result2);
}

第八章 异步编程模式_第3张图片

1.3.2 使用等待句柄

IAsyncResult 提供的另一个属性 AsyncWaitHandle 也可以实现同样的目的。

AsyncWaitHandle 是一个等待句柄(WaitHandle)对象,它定义了一系列重载的 WaitOne 方法,我们将使用的一个如下所示:

public virtual bool WaitOne(int millisecondsTimeout);

当调用以上形式的 WaitOne 方法时,调用者线程将在由方法参数 millisecondsTimeout 指定的时间段内等待 “等待句柄” 对象的状态转为 Signaled,此时 WaitOne 方法返回 true;如果超时,WaitOne 方法将返回 false。

IAsyncResult ar1 = del.BeginInvoke("Hello", "World", null, null);
IAsyncResult ar2 = del.BeginInvoke("C#", ".NET", null, null);
Console.Write("正在计算,请耐心等待");
while (!(ar1.AsyncWaitHandle.WaitOne(1000) && ar2.AsyncWaitHandle.WaitOne(1000)))
{
   Console.Write(".");
}

第八章 异步编程模式_第4张图片

1.3.3 异步回调

委托的 BeginInvoke 方法包含一个 AsyncCallback 委托类型参数,如果该参数不为空,当异步调用执行完毕后,系统会自动调用该委托,通知调用线程异步调用已结束。

BeginInvoke 方法定义中的最后两个参数 “AsyncCallback callback” 和 “object asyncState” 就是用于异步调用的。

class Program
{
   static void Main(string[] args)
   {
       Func del = null;
       MyClass m_obj = new MyClass();
       del = m_obj.CombineString;
       del.BeginInvoke("Hello", "World", CallBackShow, del);
       del.BeginInvoke("C#", ".NET", CallBackShow, del);
       Console.WriteLine("正在计算,请耐心等待");
       Console.ReadKey(); // 主线程退出时,异步调用将自动销毁(多线程编程中的背景线程)
   }

   public static void CallBackShow(IAsyncResult ar)
   {
       Func del = ar.AsyncState as Func;
       string result = del.EndInvoke(ar);
       Console.WriteLine(result);
   }
}

class MyClass
{
   public string CombineString(string first, string second)
   {
       Thread.Sleep(500);
       return first + "-" + second;
   }
}

第八章 异步编程模式_第5张图片

注:定义的回调方法 ,其返回值为 void,且输入参数只能为一个 IAsyncResult 类型。在方法体中调用 EndInvoke 方法以取回方法的执行结果。另外,ar 参数的 AsyncState 属性包含了外界传入的参数信息(即 BeginInvoke 方法的 object asyncState 参数被包装到自动创建的一个 IAsyncResult 类型的对象中,并作为方法实参自动传送给回调方法)。

第八章 异步编程模式_第6张图片

如上图所示,在回调方法 AsyncCallback 中执行 EndInvoke 方法,获得委托异步调用的结果,我们既可以直接使用该结果,也可以通知调用线程。显而易见,图中调用线程从来都不会失去执行控制权。

class Program
{
   static void Main(string[] args)
   {
       MyClass m_obj = new MyClass();
       Thread m_Thread = new Thread(__PlayThread);
       m_Thread.Start(m_obj);
       m_Thread.IsBackground = true; // 设置为背景线程(主线程退出时,辅助线程退出) 默认为 false
       Console.WriteLine("正在计算,请耐心等待");
   }

   static void __PlayThread(object selfClass)
   {
       string result = (selfClass as MyClass).CombineString("Hello", "World");
       Console.WriteLine(result);
   }
}

m_Thread.IsBackground = true;

第八章 异步编程模式_第7张图片

m_Thread.IsBackground = false ; // 主线程等所有辅助线程运行完成后退出

第八章 异步编程模式_第8张图片

1.4 处理异步调用时的异常

同步调用委托时,方法抛出来的异常直接可以由当前调用线程捕获,异步调用委托时,由于方法实际运行在其它线程中(线程池中的某一线程,非当前调用线程),因此当前线程捕获不了异常,那么我们怎样知道异步调用过程是否有异常呢?答案就在 EndInvoke 方法上,如果异步调用过程有异常,那么该异常会在我们调用 EndInvoke 方法时抛出,所以我们在调用 EndInvoke 方法时,一定要把它放在 try/catch 块中。

public static void CallBackShow(IAsyncResult ar)
{
   Func del = ar.AsyncState as Func;
   string result = "";
   try
   {
       result = del.EndInvoke(ar);
   }
   catch (System.Exception ex)
   {
       Console.WriteLine("Exception throw!");
   }      
   Console.WriteLine(result);
}

如上代码所示,EndInvoke 方法的调用放在 try/catch 块中后,异步调用过程中抛出的异常均由该 try/catch 块捕获。

因此,得到一个基本结论,不管有没有定义异步回调方法:

请在 EndInvoke 方法所在的代码处捕获异步调用抛出的异常。

1.5 异步编程小结

虽然在程序中适当采用异步编程模式,能够提高系统性能、增加系统吞吐量,但是异步编程使程序的运行流程不再呈“直线流水”型,它打乱了代码的调用关系,代码调用逻辑也变得复杂难以理解。 在异步编程过程中, 需要注意以下三点:
(1)异步编程中,除了调用线程外,几乎看不见其它任何线程,但是,我们编写的绝大部分代码却都是运行在其它线程中,比如使用委托异步调用方法时,这个方法就运行在线程池中的某个线程中,还有如果我们调用 BeginInvoke 或者 FileStream.BeginRead 方法时给定了AsyncCallback 类型参数,那么这个参数代表的回调方法也会运行在其它线程中。只要涉及到多线程同时访问同一个资源时,我们就应该注意“线程安全”问题。在 Winform 开发中,上述这些方法中都不应该包含直接访问界面的代码。
(2)系统维护的线程池是有大小限制的,我们不能无限制地开启异步调用过程,这很有可能耗尽线程池资源,从而达不到想要的效果。因此,在有些场合,数量众多、耗时太长的操作最好不要使用异步编程,而改成直接使用多线程的方法(Worker-Thread)。
(3)异步调用过程中,如果我们需要使用异步执行的结果,最好等在 EndInvoke 方法返回之后,因为该方法返回之后,才能说明异步调用已结束,否则异步调用就有可能还没有完成。EndInvoke 方法不仅能够返回异步执行的结果, 它的返回还是异步执行过程完成的标志。

异步编程与多线程编程的区别与联系

异步编程与多线程编程的效果类似,都是为了能够并行执行代码,达到同时处理任务的目的。 异步编程时,系统自己通过线程池来分配线程,不需要人工干预,异步编程逻辑复杂不易理解,而多线程编程时,完全需要人为去控制,相对较灵活。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(.NET,开发要点精讲)