zoukankan      html  css  js  c++  java
  • 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(() => 
                    {
                        //更新控件的具体代码
                    });

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

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

  • 相关阅读:
    accpet和connect设置超时
    两个模块的函数如何相互调用?
    有头结点的双向链表
    信号量PV操作实现进程间同步与互斥
    linux read write函数
    函数用指针传参挂死分析
    TCP/IP为什么需要四次握手和三次挥手
    负数在内存中的表示
    malloc的堆内存挂死原因;负数的表示
    Makefiel----no rule to make target 错误问题
  • 原文地址:https://www.cnblogs.com/xyz0835/p/9156709.html
Copyright © 2011-2022 走看看