C#异步跨线程

来源:互联网 发布:广元外卖软件 编辑:程序博客网 时间:2024/06/08 16:15

最近学习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);//实现异步,相当于创建一个后台的工作线程来等待更新界面        }    }}
0 0