一般在执行长时间的方法代码时,不想该方法阻塞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(() => { //更新控件的具体代码 });
以上内容基本能满足普通开发的多线程需求。
但如果涉及到线程取消,线程同步等要求,则需要更深入的学习了。