最近学习C#的异步操作,总是搞不明白什么意思,然后今天想自己写一下整理一下思路。
在窗体编程中,经常会碰到界面假死的状态,原因是什么呢?
首先我们看一个造成假死的一个例子
我写了一个工程,目的是在点击开始按钮后progressBar.Value每隔10秒加一,然后richTextBox打印出来当前的progressBar.Value
部分代码如下
private void UpdataUI()
{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(1 * 1000); //长时间请求
progressBar.Value = i;
richTextBox.AppendText(i.ToString() + "\n");
richTextBox.SelectionStart = richTextBox.Text.Length;
richTextBox.ScrollToCaret();
}
}
private void btn_Start_Click(object sender, EventArgs e)
{
UpdataUI();
}
此时如果运行程序,点击开始后,界面将会造成假死
其次我们来讨论一下Windows的消息机制
我们知道窗体中用户的更种操作都是基于消息传输的,在Win32中我们差不多都会见到这段代码
/* Enter the modified message loop */
while (GetMessage (&msg, NULL, 0, 0))
{
if (!TranslateMDISysAccel (hwndClient, &msg) &&
!TranslateAccelerator (hwndFrame, hAccel, &msg))
{
TranslateMessage (&msg);
DispatchMessage (&msg);
}
}
上面的意思就是说GetMessage()将消息队列中的消息依次取出,然后转换成可以理解的东西,然后分发出去执行在switch中执行自己的代码
switch (message)
{
case WM_CREATE:
case WM_COMMAND:
case WM_PAINT:
.
.
.
.......
}
用户的输入就是靠消息循环来执行的,如果现在有一个很耗时的操作也在消息循环中执行,即我们通常所说的UI线程中,那么后面的操作就会得不到响应,上面的代码中等待10秒的操作就是在UI线程中执行的,所以出现了所谓的假死,碰到这种情况就需要异步操作。
现在我们来用异步的方法解决这个问题
当你定义一个委托后,编译器会自动的为你生成一个类,还会为你在这个类里提供一个BeginInvoke方法和一个EndInvoke方法,这两个方法的实现是由CLR提供的,而这个BeginInvoke和EndInvoke只是起一个包装的作用。现在我们就用这两个方法来实现异步操作
public Form1()
{
InitializeComponent();
checkForIllegalCrossThreadCalls = false;//先加上,后面解释
}
private void UpdataUI()
{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(1 * 1000); //长时间请求
progressBar.Value = i;
richTextBox.AppendText(i.ToString() + "\n");
richTextBox.SelectionStart = richTextBox.Text.Length;
richTextBox.ScrollToCaret();
}
}
private void btn_Start_Click(object sender, EventArgs e)
{
Action act = new Action(UpdataUI);
act.BeginInvoke(null, null);//实现异步,相当于创建一个后台的工作线程来等待更新界面
}
我们发现界面不在假死,一切正常了。
上面的异步操作没有返回值,但是通常我们在异步后会等待一个返回值,我们稍微修改一下代码让其有一个返回值
public Form1()
{
InitializeComponent();
checkForIllegalCrossThreadCalls = false;//先加上,后面解释
}
private string UpdataUI()
{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(1 * 1000); //长时间请求
//ProgressBarCalc(i);
richTextBox.AppendText(i.ToString() + "\n");
progressBar.Value = i;
}
return "Finshed";
}
private void btn_Start_Click(object sender, EventArgs e)
{
Func<string> act = new Func<string>(UpdataUI);
IAsyncResult asyncResult = act.BeginInvoke((result) =>
{
string ret = act.EndInvoke(result);
MessageBox.Show(ret);
}, null);//实现异步,相当于创建一个后台的工作线程来等待更新界面
}
在这里来说明一下BeginInvoke
此方法将“异步”执行委托所指向的那个方法。所谓“异步”,就是结果并不是像调用“Invoke”方法一样直接就出现结果,BeginInvoke将在内部开辟一个新线程(注意:是后台线程!)去执行这个委托方法。因为是后台线程,因此如果主程序一旦关闭或者停止,无论后台线程的任务是否执行完毕,都将自动终止。本示例因为是WinForm,一旦运行主线程不会自动结束,但是对于诸如控制台一类的程序则不然,因此控制台程序如果使用BeginInvoke方法,必须要在其后面调用对应的EndInvoke。EndInvoke方法会阻塞当前进程,直至BeginInvoke执行完毕所有的委托方法后直接返回一个结果。
在上面的程序中我们加入了一句代码
checkForIllegalCrossThreadCalls = false;
那么这句是干什么用的呢,不妨先去掉然后运行一下,突然就报异常了,如何跨线程调用Windows窗体控件
通过调试我们发现程序主要检查checkForIllegalCrossThreadCalls、inCrossThreadSafeCall两个字段以及InvokeRequired,InvokeRequired的职责是判断当前运行的线程是不是与窗体主线程是同一个线程。
通过字面理解checkForIllegalCrossThreadCalls的意思就是“跨线程调用时是不是检查”。而这个字段在Control的静态构造函数里被设置为:checkForIllegalCrossThreadCalls = Debugger.IsAttached(checkForIllegalCrossThreadCalls 还通过CheckForIllegalCrossThreadCalls属性公开了 ),现在明白了为啥在Visual Studio里调试程序报异常,而独立运行却不报了吧。
那么该怎么解决呢,我们还是用异步的方法解决,但是现在要用Control.Invoke和Control.BeginInvoke这个是干什么的呢?这个的创建的异步线程是指相对于创建线程的线程是异步的,说的简单点就是指我们在UI线程中使用delegate.BeginInvoke的方法创建了一个相对于UI线程异步的线程,在异步线程中创建了一个相对于delegate.BeginInvoke异步的线程,即UI线程(我真心讲不清楚了,也不知道具体原因)。
private void ProgressBarCalc(int value)
{
if(this.InvokeRequired)//判断是否与UI线程在同一线程,如果不是就创建一个代理
{
this.Invoke(new Action<int>(ProgressBarCalc),value);
}
else//如果是就直接更新
{
richTextBox.AppendText(value.ToString() + "\n");
progressBar.Value = value;
}
}
再次运行后我们发现一起OK
献出所有代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace WindowsFormsApplication9
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void ProgressBarCalc(int value)//通过异步更新界面,跨线程调用Windows窗体控件
{
if(this.InvokeRequired)//判断是否与UI线程在同一线程,如果不是就创建一个代理
{
this.Invoke(new Action<int>(ProgressBarCalc),value);
}
else//如果是就直接更新
{
richTextBox.AppendText(value.ToString() + "\n");
progressBar.Value = value;
}
}
private string UpdataUI()//长时间的操作
{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(1 * 1000); //长时间请求
ProgressBarCalc(i); //更新界面
}
return "Finshed";
}
private void btn_Start_Click(object sender, EventArgs e)
{
Func<string> act = new Func<string>(UpdataUI);
IAsyncResult asyncResult = act.BeginInvoke((result) =>
{
string ret = act.EndInvoke(result);//执行完异步操作后得到返回结果
MessageBox.Show(ret);//显示出结果
}, null);//实现异步,相当于创建一个后台的工作线程来等待更新界面
}
}
}