前言
在. Net 程序开发过程中, 我们经常会遇到如下场景:
编写 WinForm 程序客户端, 需要查询数据库获取数据, 于是我们根据需求写好了代码后, 点击查询, 发现界面卡死, 无法响应. 经过调试, 发现查询数据库这一步执行了很久, 在此过程中, UI 被阻塞, 无法响应任何操作.
如何解决此问题? 我们需要分析问题成因: 在 WinForm 窗体运行时, 只有一个主线程, 即为 UI 线程, UI 线程在此过程中既负责渲染界面, 又负责查询数据, 因此在大量耗时的操作中, UI 线程无法及时响应导致出现问题. 此时我们需要将耗时操作放入异步操作, 使主线程继续响应用户的操作, 这样可以大大提升用户体验.
直接编写异步编程也许不是一件轻松的事, 和同步编程不同的是, 异步代码并不是始终按照写好的步骤执行, 且如何在异步执行完通知前序步骤也是其中一个问题, 因此会带来一系列的考验.
幸运的是, 在. Net Framework 中, 提供了多种异步编程模型以及相关的 API, 这些模型的存在使得编写异步程序变得容易上手. 随着 Framework 的不断升级, 相应的模型也在不断改进, 下面我们一起来回顾一下. Net 异步编程的前世今生.
第一个异步编程模型: APM
概述
APM, 全称 Asynchronous Programing Model, 顾名思义, 它即为异步编程模型, 最早出现于. Net Framework 1.x 中.
它使用 IAsyncResult 设计模式的异步操作, 一般由 BeginOperationName 和 EndOperationName 两个方法实现, 这两个方法分别用于开始和结束异步操作, 例如 FileStream 类中提供了 BeginRead 和 EndRead 来对文件进行异步字节读取操作.
使用
在程序运行过程中, 直接调用 BeginOperationName 后, 会将所包含的方法放入异步操作, 并返回一个 IAsyncResult 结果, 同时异步操作在另外一个线程中执行.
每次在调用 BeginOperationName 方法后, 还应调用 EndOperationName 方法, 来获取异步执行的结果, 下面我们一起来看一个示例:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading;
- using System.Threading.Tasks;
- namespace APMTest
- {
- class Program
- {
- public delegate void ConsoleDelegate();
- static void Main(string[] args)
- {
- ConsoleDelegate consoleDelegate = new ConsoleDelegate(ConsoleToUI);
- Thread.CurrentThread.Name = "主线程 Thread";
- IAsyncResult ar = consoleDelegate.BeginInvoke(null, null);
- consoleDelegate.EndInvoke(ar);
- Console.WriteLine("我是同步输出, 我的名字是:" + Thread.CurrentThread.Name);
- Console.Read();
- }
- public static void ConsoleToUI()
- {
- if (Thread.CurrentThread.IsThreadPoolThread)
- {
- Thread.CurrentThread.Name = "线程池 Thread";
- }
- else
- {
- Thread.CurrentThread.Name = "普通 Thread";
- }
- Thread.Sleep(3000); // 模拟耗时操作
- Console.WriteLine("我是异步输出, 我的名字是:" + Thread.CurrentThread.Name);
- }
- }
- }
在这段示例中, 我们定义了一个委托来使用其 BeginInvoke/EndInvoke 方法用于我们自定义方法的异步执行, 同时将线程名称打印出来, 用于区分主线程与异步线程.
如代码中所示, 在调用 BeginInvoke 之后, 立即调用了 EndInvoke 获取结果, 那么会发生什么呢?
如下图所示:
看到这里大家也许会比较诧异: 为什么同步操作会在异步操作之后输出呢? 这样不是和同步就一样了吗?
原因是这样的: EndInvoke 方法会阻塞调用线程, 直到异步调用结束, 由于我们在异步操作中模拟了 3s 耗时操作, 所以它会一直等待到 3s 结束后输出异步信息, 此时才完成了异步操作, 进而进行下一步的同步操作.
同时在 BeginInvoke 返回的 IAynscResult 中, 包含如下属性:
通过轮询 IsCompleted 属性或使用 AsyncWaitHandle 属性, 均可以获取异步操作是否完成, 从而进行下一步操作, 相关代码如下所示:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading;
- using System.Threading.Tasks;
- namespace APMTest
- {
- class Program
- {
- public delegate void ConsoleDelegate();
- static void Main(string[] args)
- {
- ConsoleDelegate consoleDelegate = new ConsoleDelegate(ConsoleToUI);
- Thread.CurrentThread.Name = "主线程 Thread";
- IAsyncResult ar = consoleDelegate.BeginInvoke(null, null);
- // 此处改为了轮询 IsCompleted 属性, AsyncWaitHandle 属性同理
- while (!ar.IsCompleted)
- {
- Console.WriteLine("等待执行...");
- }
- consoleDelegate.EndInvoke(ar);
- Console.WriteLine("我是同步输出, 我的名字是:" + Thread.CurrentThread.Name);
- Console.Read();
- }
- public static void ConsoleToUI()
- {
- if (Thread.CurrentThread.IsThreadPoolThread)
- {
- Thread.CurrentThread.Name = "线程池 Thread";
- }
- else
- {
- Thread.CurrentThread.Name = "普通 Thread";
- }
- Thread.Sleep(3000); // 模拟耗时操作
- Console.WriteLine("我是异步输出, 我的名字是:" + Thread.CurrentThread.Name);
- }
- }
- }
运行后结果如下:
可以发现, 在轮询属性时, 程序仍然会等待异步操作完成, 进而进行下一步的同步输出, 无法达到我们需要的效果, 那么究竟有没有办法解决呢?
此时我们需要引入一个新方法: 使用回调.
在之前的操作中, 使用 BeginInvoke 方法, 两个参数总传入的为 null. 若要使用回调机制, 则需传入一个类型为 AsyncCallback 的回调函数, 并在最后一个参数中, 传入需要使用的参数, 如以下代码所示:
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading;
- using System.Threading.Tasks;
- namespace APMTest
- {
- class Program
- {
- public delegate void ConsoleDelegate();
- static void Main(string[] args)
- {
- ConsoleDelegate consoleDelegate = new ConsoleDelegate(ConsoleToUI);
- Thread.CurrentThread.Name = "主线程 Thread";
- // 此处传入 AsyncCallback 类型的回调函数, 并传入需要使用的参数
- consoleDelegate.BeginInvoke(CallBack, consoleDelegate);
- //IAsyncResult ar = consoleDelegate.BeginInvoke(null, null);
- //// 此处改为了轮询 IsCompleted 属性, AsyncWaitHandle 属性同理
- //while (!ar.IsCompleted)
- //{
- // Console.WriteLine("等待执行...");
- //}
- //consoleDelegate.EndInvoke(ar);
- Console.WriteLine("我是同步输出, 我的名字是:" + Thread.CurrentThread.Name);
- Console.Read();
- }
- public static void ConsoleToUI()
- {
- if (Thread.CurrentThread.IsThreadPoolThread)
- {
- Thread.CurrentThread.Name = "线程池 Thread";
- }
- else
- {
- Thread.CurrentThread.Name = "普通 Thread";
- }
- Thread.Sleep(3000); // 模拟耗时操作
- Console.WriteLine("我是异步输出, 我的名字是:" + Thread.CurrentThread.Name);
- }
- public static void CallBack(IAsyncResult ar)
- {
- // 使用 IAsyncResult 的 AsyncState 获取 BeginInvoke 中的参数, 并用于执行 EndInvoke
- ConsoleDelegate callBackDelegate = ar.AsyncState as ConsoleDelegate;
- callBackDelegate.EndInvoke(ar);
- }
- }
- }
运行后结果如下:
此时可以看出, 使用回调的方式已经实现了我们需要的效果. 在同步执行时, 将耗时操作放入异步操作, 从而不影响同步操作的继续执行, 在异步操作完成后, 回调返回相应的结果.
小结
APM 模型的引入, 使得编写异步程序变的如此简单, 只需定义委托, 将要执行的方法包含其中, 并调用 Begin/End 方法对, 即可实现异步编程. 在一些基础类库中, 也已经提供了异步操作的方法, 直接调用即可.
同时我们可以看到, BeginInvoke 方法, 实际上是调用了线程池中的线程进行操作, 因此 APM 模型也应属于多线程程序, 同时包含主线程与线程池线程.
但是 APM 模型也存在一些缺点:
1, 若不使用回调机制, 则需等待异步操作完成后才能继续执行, 此时未达到异步操作的效果.
2, 在异步操作的过程中, 无法取消, 也无法得知操作进度.
3, 若编写 GUI 程序, 异步操作内容与主线程未在同一线程, 操作控件时会引起线程安全问题.
为了解决这些缺陷, 微软推出了其他的异步模式, 预知后事如何, 且听下回分解.
下集预告
浅谈. Net 异步编程的前世今生 ----EAP 篇
来源: https://www.cnblogs.com/wackysoft/p/10777264.html