C# 线程 Thread及ThreadPool,以及跨线程更新控件

一般在执行长时间的方法代码时,不想该方法阻塞UI或者不想阻塞其他方法时,可以考虑使用线程。

 

线程 Thread 的常用写法

 

1.第一种传统方法,先声明 LongTimeWork方法,再通过线程调用

        private void button1_Click(object sender, EventArgs e)
        {
            Thread th1 = new Thread(LongTimeWork); //1. 声明线程
            th1.Start(); //2.开始线程
        }

        private void LongTimeWork()
        {
            //方法代码
            MessageBox.Show("做了点微小的工作");
        }

2. 第二种,使用匿名委托,不用单独声明方法,更为便捷

            Thread th1 = new Thread(new ThreadStart(delegate  //1.声明线程
            {
                //方法代码
                MessageBox.Show("做了点微小的工作");
            }));

            th1.Start(); //2. 开始线程     

3. 第三种,使用Lambda表达式

            Thread th1 = new Thread(() =>  //1.声明线程
            {
                //方法代码
                MessageBox.Show("做了点微小的工作");
            });
      
            th1.Start(); //2. 开始线程

 

在使用习惯后,使用第三种方式来调用最为方便。

 

线程池 ThreadPool的常用写法

 

第一种,传统方法:先声明LongTimeWorkItem方法,带一个object参数,再通过ThreadPool的QueueUserWorkItem方法添加到队列中

        private void button4_Click(object sender, EventArgs e)
        {
            ThreadPool.QueueUserWorkItem(LongTimeWorkItem); //线程池队列中添加工作项
        }

        private void LongTimeWorkItem(object state)
        {
            //方法代码
            MessageBox.Show("做了点微小的工作");
        }

第二种,直接使用匿名委托,不用再单独声明方法

            ThreadPool.QueueUserWorkItem(delegate
            {
                //方法代码
                MessageBox.Show("做了点微小的工作");
            });      

第三种,使用Lambda表达式

            ThreadPool.QueueUserWorkItem(state => 
            {
                //方法代码
                MessageBox.Show("做了点微小的工作");
            });

使用习惯后,选择第三种方法较为便捷。

 

关于如何传参

 

虽然在线程及线程池调用时,有带参数的委托可以使用 (ParameterizedThreadStart和WaitCallback) ,但都只能传入一个object的参数。

如果想传入两个或更多的参数时,则还需要声明一个类并将参数声明为变量并赋值再传入,这无疑是增加了开发成本。并且在方法里,还需要讲object参数传换为实际的类,也比较麻烦。

而实际上我们有更简便的方法,在线程中直接使用外部的局部变量,类变量或静态变量。下面是一个例子

        string str1 = "做了点";

        static string str2 = "微小的";

        private void button7_Click(object sender, EventArgs e)
        {
            string str3 = "工作";

            Thread th1 = new Thread(new ThreadStart(delegate  //1.声明线程
            {
                //方法代码
                MessageBox.Show(str1 + str2 + str3);
            }));

            th1.Start(); //2. 开始线程
        }

str1是窗体中的变量,str2是静态变量,str3是局部变量。在这里我并没有使用ParameterizedThreadStart来传入参数,但达到了传入参数的效果,并且一次“传入”3个参数。

结论就是:直接使用外部的变量即可,不用刻意传参

ThreadPool传参也可以使用该方法。

 

Thread和ThreadPool的区别

 

主要区别有两个

1.Thread默认是前台线程,ThreadPool是后台线程。前台线程的意思是:在整个程序退出时,如果有前台线程的方法未执行完毕,那么该线程会继续执行直到完毕。 而后台线程则会在程序退出时强制停止。

   需要注意的是,可以通过修改Thread的IsBackground属性,声明为后台线程。

2. Thread在Start后会立即执行,而ThreadPool在调用QueueUserWorkItem方法后,则会加入到队列中,由系统决定执行时间。

    如果在线程池中已经有很多方法在执行,则新加入的方法可能需要在前面的方法执行完毕后才能执行。

 

跨线程更新控件

 

这是一个经常遇到的问题,因为我们在执行多线程方法时,可能需要在界面上展示执行进度,或者展示执行结果。

但如果直接在线程中更新控件的话,通常会遇到类似“跨线程操作无效,从不是创建"label1"的线程访问它”这样的错误,比如如下的代码:

        private void button8_Click(object sender, EventArgs e)
        {
            Thread th1 = new Thread(()=>  
            {
                //方法代码
                button8.Text = "做了点微小的工作";  //异常:线程间操作无效: 从不是创建控件“button8”的线程访问它。
            });

            th1.Start(); 
        }

 

这是因为.Net禁止了跨线程操作控件。那么我们需要使用其他的方法来实现,那就是Control类中的Invoke方法,而Invoke方法要求调用委托。我们还是一步一步来,从传统方法到最简便的写法

第一种写法,先声明更新控件的方法,再只用Invoke方法调用

        /// <summary>
        /// 更新控件的文本
        /// </summary>
        private void UpdateButtonText()
        {
            button8.Text = "做了点微小的工作";
        }

        private void button8_Click(object sender, EventArgs e)
        {
            Thread th1 = new Thread(()=>
            {
                //方法代码

                button8.Invoke((MethodInvoker)UpdateButtonText);
            });

            th1.Start(); 
        }

 

编译执行,顺利运行,并且控件也进行了更新。但是这种方法依然很麻烦,每次更新控件需要再声明方法,如果需要频繁更新控件,会使代码冗长又难以读懂。

 

那么我们继续使用匿名委托,第二种写法:匿名委托

        private void button8_Click(object sender, EventArgs e)
        {
            Thread th1 = new Thread(()=>
            {
                //方法代码

                button8.Invoke((MethodInvoker)delegate
                {
                    button8.Text = "做了点微小的工作";
                });
            });

            th1.Start(); 
        }

同样,该方法可以顺利运行并实现了跨线程更新控件,并且省去了声明方法的步骤,简便了许多。

 

第三种写法:按照常理,这里应该使用最简单的Lambda表达式了,但是这里又有些不一样,无法直接使用MethodInvoker进行强制转换,那么我们使用点其他的技巧,并且把一些控件检测的工作一并做了

 

首先,在静态类中声明一个扩展方法,用于跨线程更新控件,传入的参数改用MethodInvoker委托。

    public static class ControlExtension
    {
        public static void InvokeMethod(this Control control, MethodInvoker method)
        {
            if (!control.IsHandleCreated) //句柄不可用,退出
                return;

            if (control.IsDisposed) //控件已释放,退出
                return;

            if (control.InvokeRequired) //必须调用Invoke方法来更新控件, 则通过Invoke来执行方法
            {
                control.Invoke(method);
            }
            else  //不必调用Invoke方法,则直接执行方法
            {
                method();
            }
        }
    }

在这个方法前面,添加了对于控件更新前的条件检查,不满足则直接退出,停止执行。并且这里涉及到InvokeRequired属性及扩展方法的概念。可以百度下进行了解

那么我们在调用这个方法进行跨线程更新控件时,就变成了这样:

        private void button8_Click(object sender, EventArgs e)
        {
            Thread th1 = new Thread(()=>
            {
                //方法代码

                button8.InvokeMethod(() => //调用扩展方法,并且可以使用Lambda表达式
                {
                    button8.Text = "做了点微小的工作";
                });
            });

            th1.Start(); 
        }

需要注意的是,如果直接使用窗体实例的Invoke方法来更新子控件,也能顺利运行,比如:

        private void button8_Click(object sender, EventArgs e)
        {
            Thread th1 = new Thread(() =>
            {
                //方法代码

                this.InvokeMethod(() => //这里的this, 即是窗体实例
                {
                    //更新控件的具体代码
                    button1.Text = "做了点";
                    button2.Text = "微小的";
                    button3.Text = "工作";
                });
            });

            th1.Start();
        }

直接通过窗体实例this调用方法实现了跨线程更新多个控件,这种情况也会经常遇到。所以一般情况下,直接通过this调用来更新控件即可。

 

那么到这里为止,则是得到了最为简便的跨线程更新控件的写法,重点就是这么一句: 

                this.InvokeMethod(() => 
                {
                    //更新控件的具体代码
                });

 

以上内容基本能满足普通开发的多线程需求。

 

但如果涉及到线程取消,线程同步等要求,则需要更深入的学习了。

 

posted @ 2018-06-08 17:20  逍遥子k  阅读(598)  评论(0编辑  收藏  举报