C#异步编程案例学习——异步加载大资源文件1 之 使用 BeginInvoke 与 EndInvoke
本文主要是根据一个项目中遇到的异步的小例子,研究学习异步编程模型(APM)模式如何使用 BeginInvoke 与 EndInvoke。
C# 中 BeginInvoke 与 EndInvoke 的一个简单的使用案例
在使用 WPF 开发桌面软件过程中,遇到一个需要预加载大量文件的需求,具体需求如下:
- 在软件启动后,需要加载大量的 CAD 资源文件,文件供软件中后续使用。
- 文件加载过程中非常耗时,若同步加载则界面会出现假死的状态。
- 在文件加载过程中,需要保证界面仍然可以进行其他操作,屏蔽跟资源文件有关的操作。
- 文件加载完成后,开放跟资源文件有关的操作。
解决方案:
在软件启动后,软件页面正常完成加载后,后台继续进行异步加载资源文件,此时可操作正在加载的资源文件的按钮设置为不可操作。文件加载完成之后开发操作资源文件的按钮可操作。
具体实现如下:
1、WPF 页面 Window 中存在一个事件:ContentRendered。该事件在窗口的内容呈现完毕之后发生。定义一个方法,方法中进行文件加载的操作,并将该方法绑定到 ContentRendered 事件。
代码如下:
其中,LoadingResources() 方法为实际加载文件的方法;
ShowProgressBarFile 方法中具体实现异步,并更改页面按钮状态
private void Window_ContentRendered(object sender, EventArgs e)
{
//禁用对应的操作按钮
this.btnThreeView.IsEnabled = false;
Func<bool> action = (() => LoadingResources());
ShowProgressBarFile(action, "文件加载中...");
}
2、方法 LoadingResources(),实现文件加载的功能。
文件加载使用多线程编程。一个线程为真加载线程,加载对应的资源文件,文件完成加载后,对应页面的操作按钮放开显示;
一个线程为假加载线程,可以用于加载进度条显示在页面。
具体详细的学习和分析见文章:
C#异步编程案例学习——异步加载大资源文件 2 之 多线程加载
private bool LoadingResources()
{
//......实现具体文件加载
Thread t1 = new Thread(new ThreadStart(loadReal)); //loadReal 是真线程加载
t1.Name = "真加载线程";
t1.IsBackground = false;
Thread t2 = new Thread(new ThreadStart(loadNotTrue)); //loadNotTrue 是假线程加载
t2.Name = "假加载线程";
t2.IsBackground = true;
t1.Start();
t2.Start();
while (trueLoad == 0 && falseLoad == 0)
{
//阻塞
}
//下面的代码是在完成加载后,屏蔽页面假线程加载显示的进度条信息
this.pb_info.Dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate ()
{
this.pb_info.Visibility = Visibility.Collapsed;
});
this.pb_test.Dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate ()
{
this.pb_test.Visibility = Visibility.Collapsed;
});
return true;
}
3、方法ShowProgressBarFile 实现异步:
其中,func.BeginInvoke 开启异步;
asyncResult.IsCompleted 用于检查异步方法是否执行完成;
func.EndInvoke 结束异步。
这种异步编程的方法叫做“异步编程模型(APM)模式”
private bool ShowProgressBarFile(Func<bool> func, string text)
{
var asyncResult = func.BeginInvoke(null, null);
while (!asyncResult.IsCompleted)
{
System.Windows.Forms.Application.DoEvents();
}
return func.EndInvoke(asyncResult);
}
异步编程模型模式 APM
.NET Framework 中提供了执行异步操作的 3 种模式:
- 基于任务的异步模式(Task-based Asynchronous Pattern, TAP)使用一种方法来表示异步操作的启动和完成。TAP 是在 .NET Framework 4 中引入的。C# 中的 async 和 await 关键词为 TAP 添加了语言支持。
- 异步编程模型(Asynchronous Programming Model)模式(也称 IAsyncResult模式),在此模式中异步操作需要 Beginxxx 和 Endxxx 方法。
- 基于事件的异步模式(Event-based Asynchronous Pattern,EAP),这种模式需要 Async 后缀,也需要一个或多个事件、事件处理程序委托类型和 EventArg 派生类型。
APM 和 EAP 两种模式是早期版本引入的,.Net 4 版本之后,Visual Studio 建议对于新的开发工作不再使用它们。对于 TAP 在《C#异步编程学习笔记》的文章中有相关学习笔记,后续详细学习使用后补充学习博客。
本文主要介绍的 BeginInvoke 与 EndInvoke 方式是基于 APM 的。.net中有很多类实现了该模式(比如HttpWebRequest),同时我们也可以自定义类来实现APM模式(继承IAsyncResult接口并且实现BeginXXX和EndXXX方法)。
异步编程中的 BeginInvoke 和 EndInvoke
运行原理
如果委托对象的调用列表中只有一个方法(引用方法),就可以异步执行这个方法,通过调用委托类特有的两个方法 BeginInvoke 和 EndInvoke 去执行。
当我们调用委托的 BeginInvoke 方法时,它开始在一个独立线程上执行引用方法,并且立即返回到原始线程。原始线程可以继续,而引用方法会在线程此的线程中并行执行。
当程序希望获取已完成的异步方法的结果时,可以检测 BeginInvoke 返回的 IAsyncResult 的 IsCompleted 属性,或调用委托的 EndInvoke 方法来等待委托完成。
BeginInvoke方法
IAsyncResult BeginInvoke(-------,AsyncCallback callback,object state)
- 在调用BeginInvoke时,参数列表中的实参组成如下:
- 引用方法需要的参数。
- 两个额外的参数——callback参数和state参数。
- callback,回调方法的名字;
- state可以为null,或发送给回调函数的参数的对象,参数类型是object,在回调函数中必须转换成正确的类型。
- BeginInvoke从线程池中获取一个线程并且在新的线程开始时运行引用方法。
- BeginInvoke返回给调用线程一个实现IAsyncResult接口的对象。这个接口引用包含了异步方法的当前状态,原始线程然后可以继续执行。
EndInvoke方法
- 它接受一个由BeginInvoke方法返回的IAsyncResult对象的引用,并找到它关联的线程。
- 如果线程池的线程已经退出,EndInvoke做如下的事情。
- 它清理退出线程的状态并且释放它的资源。
- 它找到引用方法返回的值并且把它的值作为返回值。
- 如果当EndInvoke被调用时线程池的线程仍然在运行,调用线程就会停止并等待,直到清理完毕并返回值。因为EndInvoke是为开启的线程进行清理,所以必须确保对每一个BeginInvoke都调用EndInvoke。
- 如果异步方法触发了异常,在调用EndInvoke时会抛出异常。
AsyncResult类
当调用委托的BeginInvoke方法时,系统会创建一个AsyncResult类对象, BeginInvoke方法执行完后,方法返回属于AsyncResult类对象中的一个IAsyncResult接口类型的引用。IAsyncResult 是使用 BeginInvoke 和 EndInvoke 方法的必要部分。
- 当我们调用委托对象的BeginInvoke方法时,系统创建了一个AsyncResult类的对象。然而,它不返回类的对象的引用,而是返回对象中包含的IAsyncResult接口的引用。
- AsyncResult对象包含一个叫做AsyncDelegate的属性,它返回一个指向被调用来开启异步方法的委托的引用。但是这个属性是类对象的一部分而不是接口的一部分。
- IsCompleted属性返回一个布尔值,表示异步方法是否完成。
- AsyncState属性返回一个对象的引用,它被作为 BeginInvoke 方法调用时的state参数。它返回object类型的引用。
BeginInvoke和EndInvoke 的三种模式
- 等待模式,在发起了异步方法以及做了一些其它处理之后,原始线程就中断,并且等待异步方法完成之后再继续。
- 轮询模式,原始线程定期检查发起的线程是否完成,如果没有则可以继续做一些其它的事情。
- 回调模式,原始线程一直在执行,无需等待或检查发起的线程是否完成。在发起的线程中的引用方法完成之后,发起的线程就会调用回调方法,由回调方法在调用EndInvoke之前处理异步方法的结构。
等待模式
在这种模式里,原始线程发起一个异步方法的调用,做一些其它处理,然后停止并等待,直到开启的线程结束。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace waitModel
{
// 1、声明一个委托
delegate int MyDel(int x, int y);
class Program
{
static int sum(int x,int y) //异步方法
{
Console.WriteLine("inside sum");
Thread.Sleep(8000);
return x+y;
}
static void Main(string[] args)
{
// 2、创建委托变量
// 3、为委托绑定一个方法
MyDel Del = new MyDel(sum);
Console.WriteLine("Before BeginInvoke");
// 4、异步调用委托。
// 开始异步执行,封装回调函数:BeginInvoke(参数,回调函数,回调函数多需的对象/参数)
IAsyncResult iresult = Del.BeginInvoke(4,6,null,null); //开始异步执行
Console.WriteLine("After BeginInvoke"); //程序在执行异步方法的同时,执行该语句
Console.WriteLine("Doing something");
int result = Del.EndInvoke(iresult); //等待,直到异步方法执行完毕,将结果
Console.WriteLine("After EndInvoke, result = {0}",result);
Console.ReadKey();
}
}
}
异步操作步骤(等待模式/轮询模式)
异步操作代码实现一般需要经过以下四个步骤,这四个步骤适用于等待模式和轮询模式。
- 声明一个委托类型
- 创建委托类型的变量
- 为委托绑定一个方法(返回值和参数列表与委托相同)
- 异步调用委托
如上四个步骤均在等待模式和轮询模式的代码中标注
轮询模式
在轮询模式中,原始线程发起了异步方法的调用,做一些其它处理,然后使用 IAsyncResult 对象的 IsCompleted 属性来定期检查开启的线程是否完成。如果异步方法已经完成,原始线程就调用 EndInvoke 并继续。否则,它做一些其它处理,然后过一会儿再检查。
轮询模式与等待模式相比,只增加了一步:通过检查 IAsnyResult 接口对象的 IsComplete 属性的判断异步方法是否完成,如没完成,在检查过程中可以做其它事情。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
//轮询模式与等待一直完成模式相比只增加了iresult.IsCompleted判断是否完成的步骤
namespace polling
{
class Program
{
// 1、声明一个委托
delegate int MyDel(int x,int y);
static int sum(int x, int y)
{
Console.WriteLine("insid sum");
Thread.Sleep(1000);
return x + y;
}
static void Main(string[] args)
{
// 2、创建委托变量
// 3、为委托绑定一个方法
MyDel Del = new MyDel(sum);
Console.WriteLine("Before BeginInvoke");
// 4、异步调用委托
IAsyncResult iresult = Del.BeginInvoke(2,3,null,null); //发起异步调用
Console.WriteLine("After BeginInvoke");
while (!iresult.IsCompleted) //检测IAsyncResult接口的IsCompleted属性判断异步方法是否完成
{
Console.WriteLine("Sum Not Complete");
Thread.Sleep(1000);
}
Console.WriteLine("Sum Completed.");
int result = Del.EndInvoke(iresult); //异步方法执行完后才会执行这一句
Console.WriteLine("Result: {0}",result);
Console.ReadKey();
}
}
}
回调模式
上面使用等待和轮询模式的方式需要等待。等待是个让人很恼火的事情。.Net考虑了这一点,为我们准备了回调的方式:你异步调用后继续干你的事儿,等你执行完后,你告我一声就ok了。
回调模式的不同之处在于,一旦主线程线程发起了异步方法,它就自己管自己了,不再考虑异步方法是否执行完成,当异步方法执行完后,系统会通过调用一个回调方法处理异步方法的结果,和调用委托的 EndInvoke 方法。
回调方法:回调方法的返回类型为void,以一个IAsyncResult接口类型的参数做为形参:
void AsyncCallback(IAsyncResult iresult)
上文提到 BeginInvoke方法的参数意义
IAsyncResult BeginInvoke(-------,AsyncCallback callback,object state)
- 第一个参数callback,回调方法的名字
- 第二个参数state可以为null,或发送给回调函数的参数的对象,参数类型是object,在回调函数中必须转换成正确的类型。
回调方法和BeginInvoke方法是通过BeginInvoke方法内部的最后两个参数关联的
两种方法为BeginInvoke方法第一个参数赋值:
- 使用回调方法创建 AsyncCallback 类型的委托
- 只提供回调方法的名称,编译器会自动创建委托
回调模式是在回调方法内部,通过调用委托的 EndInvoke 方法来处理异步方法返回的结果,释放线程所占用的资源,而等待模式和轮询模式是在主线程中调用的;
要调用委托的EndInvoke方法,必须要有委托的引用,在回调方法内部,先是将回调方法的 IAsyncResult 接口类型通过里氏转换转换为 AsyncResult 类类型,然后再调用 AsyncResult 类的 AsyncDelegate 属性转化为合适的委托类型。
- 给回调方法的参数只有一个,就是刚结束的异步方法的IAsyncResult接口的引用,要记住,IAsyncResult接口对象在AsyncResult类对象的内部。
- 尽管IAsyncResult接口没有委托对象的引用,而包含它的AsyncResult类对象却有委托对象的引用。
- 有了类对象的引用,我们现在就可以调用类对象的AsyncDelegate属性并且把它转化为合适的委托类型。这样就得到了委托引用,我们可以用它来调用EndInvoke。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace callback
{
delegate int Mydel(int x,int y);
class Program
{
static int sum(int x,int y) //异步方法
{
Console.WriteLine(" InSide Sum1");
Thread.Sleep(1000);
Console.WriteLine(" InSide Sum2");
return x + y;
}
//回调模式是在回调方法内部,通过调用委托的EndInvoke方法来处理异步方法返回的结果,释放线程所占用的资源而等待直到完成模式和轮询模式是在主线程中调用;
//要调用委托的EndInvoke方法,必须要有委托的引用,在回调方法内部,先是将回调方法的IAsyncResult接口类型通过里氏转换转换为AsyncResult类类型,
//然后再调用AsyncResult类的AsyncDelegate属性转化为合适的委托类型
static void callback_(IAsyncResult iar) //回调方法 无论如何回调方法中的内容必须在异步方法执行完后才执行
{
Console.WriteLine(" Inside CallWhenDone");
AsyncResult ar = (AsyncResult)iar; //里氏转换,如果父类中装的是子类的对象,那么可以将这个父类转换为子类对象
Mydel del = (Mydel)ar.AsyncDelegate; //通过AsyncResult类对象的AsyncDelegate属性转化为合适的Mydel委托类型
long result = del.EndInvoke(iar);
Console.WriteLine(" The result is: {0}",result);
}
static void Main(string[] args)
{
Mydel Del = new Mydel(sum);
Console.WriteLine("Before BeginInvoke");
IAsyncResult iar = Del.BeginInvoke(7,8,new AsyncCallback(callback_),Del); //Del焕成null结果也一样
Console.WriteLine("Doing more work in main");
Thread.Sleep(500);
Console.WriteLine("Done with Main.Exiting");
Console.ReadKey();
}
}
}
异步操作步骤(回调模式)
异步操作代码(回调模式)相较于前面的增加了一个回调,这三个步骤适用于等待模式和轮询模式。
- 声明一个委托类型
- 创建委托类型的变量
- 为委托绑定一个方法(返回值和参数列表与委托相同)
- 异步调用委托
- 回调函数
//【1】声明一个名为Operation的委托,并交代执行方法
// 这里完成了1. 声明一个委托类型;2. 创建委托类型的变量;3. 为委托绑定一个方法
//int:传入参数 string:返回值
public Func<int, string> Operation = (num) =>
{
Thread.Sleep(num * 1000);
return num.ToString();
};
private void button1_Click(object sender, EventArgs e)
{
//【2】异步调用
IAsyncResult str = Operation.BeginInvoke(2, CallBack, "发送给回调函数的参数");
}
//【3】回调函数
public void CallBack(IAsyncResult result)
{
string res = Operation.EndInvoke(result); //返回结果(结果类型,与委托返回值类型一致)
var v = result.AsyncState; //获取传递给回调函数的参数
}
笔记参考:
http://www.cnblogs.com/zwt-blog/p/4812530.html#3611906
https://blog.csdn.net/ABC13222880223/article/details/85317621