在上一篇中我们讲解了多线程的一些基本概念,并举了一些例子,在本章中我们将会讲解线程池:ThreadPool。
在开始讲解ThreadPool之前,我们先用下面的例子来回顾一下以前讲过的Thread。
1 private void Threads_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine($"****************btnThreads_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 4 ThreadStart threadStart = () => this.DoSomethingLong("Threads_Click"); 5 Thread thread = new Thread(threadStart); 6 // 启动线程 7 thread.Start(); 8 9 Console.WriteLine($"****************btnThreads_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 10 }
上面是启动一个线程的代码,结果如下:
下面讲解一下在Thread中常见的几个方法:
1、Suspend()方法
F12查看Suspend()方法的定义:
可以看到Suspend()方法表示将线程挂起,就是将线程停止执行。上面有一个特性表示该方法已经是过时的。不建议使用该方法:因为线程挂起可能会导致死锁,线程运行的时候会占用某些资源,虽然把线程挂起了,但是资源还会占有不会释放。
2、Resume()方法
F12查看Resume()的定义:
Resume()表示唤醒挂起的线程。同样,该方法也是弃用的。
Suspend()和Resume()方法是相对的,Suspend()是挂起线程,Resume()是唤醒挂起的线程。这两个方法现在都是弃用的,不建议使用。
3、Abort()方法
F12查看定义:
从定义中可以看出:Abort()方法是终止线程,终止线程的方式是抛出异常导致线程结束。一个线程想要结束就是执行完这个线程内所有的操作,但如果这时想要结束线程,只能强制的添加一个异常,抛出异常以后使线程结束。也不建议使用Abort方法,原因有下面两点:结束线程的时候可能会有延迟;结束的时候有些操作已经发出不能终止(比如数据库增删改查的操作)。
如果一定要使用Abort()方法,必须加try-catch异常处理:
1 try 2 { 3 // 销毁线程 4 thread.Abort(); 5 } 6 catch(Exception ex) 7 { 8 // 取消异常继续计算 9 Thread.ResetAbort(); 10 }
4、Join()方法
Join()方法表示线程等待,还有一个重载的方法表示等待多长时间,请看下面的例子:
1 private void Threads_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine($"****************btnThreads_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 4 ThreadStart threadStart = () => this.DoSomethingLong("Threads_Click"); 5 Thread thread = new Thread(threadStart); 6 // 启动线程 7 thread.Start(); 8 // 线程等待 9 thread.Join(500);// 等待500毫秒 10 Console.WriteLine("等待500毫秒"); 11 thread.Join();// 当前线程等待thread完成 12 Console.WriteLine($"****************btnThreads_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 13 }
结果:
5、IsBackground
IsBackground属性表示该线程是前台线程还是后台线程,默认都是前台线程。前台线程和后台线程有什么区别呢?
前台线程必须等待所有的线程都执行完之后才能退出,后台线程随着进程的退出而退出。
二、线程池ThreadPool
线程池ThreadPool是.NET框架在2.0的时候推出的,线程池可以重用线程,避免重复的创建和销毁。
1、QueueUserWorkItem()方法
QueueUserWorkItem()方法用来启动一个多线程。
1 // 启动多线程 2 ThreadPool.QueueUserWorkItem(p => this.DoSomethingLong("btnTrheadPool_Click"));
2、GetMaxThreads()方法
GetMaxThreads()用来获取线程池中最多可以有多少个辅助线程和最多有多少个异步线程。
1 ThreadPool.GetMaxThreads(out int workerThreads, out int completionPortThreads); 2 Console.WriteLine($"GetMaxThreads workerThreads={workerThreads} completionPortThreads={completionPortThreads}");
3、GetMinThreads()方法
GetMinThreads()用来获取线程池中最少可以有多少个辅助线程和最少有多少个异步线程。
1 ThreadPool.GetMinThreads(out int workerThreads, out int completionPortThreads); 2 Console.WriteLine($"GetMinThreads workerThreads={workerThreads} completionPortThreads={completionPortThreads}");
4、SetMaxThreads()和SetMinThreads()
SetMaxThreads()和SetMinThreads()分别用来设置线程池中最多线程数和最少线程数。
1 private void btnTrheadPool_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine($"****************btnThreadPool_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 4 // 启动多线程 5 ThreadPool.QueueUserWorkItem(p => this.DoSomethingLong("btnTrheadPool_Click")); 6 Console.WriteLine("输出系统默认最多线程数和最少线程数"); 7 // 最大线程 8 ThreadPool.GetMaxThreads(out int workerMaxThreads, out int completionPortMaxThreads); 9 Console.WriteLine($"GetMaxThreads workerThreads={workerMaxThreads} completionPortThreads={completionPortMaxThreads}"); 10 11 // 最小线程 12 ThreadPool.GetMinThreads(out int workerMinThreads, out int completionPortMinThreads); 13 Console.WriteLine($"GetMinThreads workerThreads={workerMinThreads} completionPortThreads={completionPortMinThreads}"); 14 Console.WriteLine("************设置最多线程数和最少线程数****************"); 15 // 设置最大线程 16 ThreadPool.SetMaxThreads(16, 16); 17 // 设置最小线程 18 ThreadPool.SetMinThreads(8, 8); 19 Console.WriteLine("输出修改后的最多线程数和最少线程数"); 20 ThreadPool.GetMaxThreads(out int workerThreads, out int completionPortThreads); 21 Console.WriteLine($"GetMaxThreads workerThreads={workerThreads} completionPortThreads={completionPortThreads}"); 22 23 ThreadPool.GetMinThreads(out int workerEditThreads, out int completionPortEditThreads); 24 Console.WriteLine($"GetMinThreads workerThreads={workerEditThreads} completionPortThreads={completionPortEditThreads}"); 25 Console.WriteLine($"****************btnThreadPool_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 26 }
结果:
5、ThreadPool实现线程等待
先来看下面一个小例子:
1 ThreadPool.QueueUserWorkItem(p => 2 { 3 this.DoSomethingLong("btnThreadPool_Click"); 4 }); 5 Console.WriteLine("等着QueueUserWorkItem完成后才执行");
我们想让异步多线程执行完以后再输出“等着QueueUserWorkItem完成后才执行” 这句话,上面的代码运行效果如下:
从截图中可以看出,效果并不是我们想要的,Thread中提供了暂停、恢复等API,但是ThreadPool中没有这些API,在ThreadPool中要实现线程等待,需要使用到ManualResetEvent类。
ManualResetEvent类的定义如下:
ManualResetEvent需要一个bool类型的参数来表示暂停和停止。上面的代码修改如下:
1 // 参数为false 2 ManualResetEvent manualResetEvent = new ManualResetEvent(false); 3 ThreadPool.QueueUserWorkItem(p => 4 { 5 this.DoSomethingLong("btnThreadPool_Click"); 6 // 设置为true,WaitOne可以通过 7 manualResetEvent.Set(); 8 }); 9 // 如果为false则过不去,为true可以通过 10 manualResetEvent.WaitOne(); 11 Console.WriteLine("等着QueueUserWorkItem完成后才执行");
结果:
ManualResetEvent类的参数值执行顺序如下:
(1)、false--WaitOne等待--Set--true--WaitOne直接过去
(2)、true--WaitOne直接过去--ReSet--false--WaitOne等待
注意:一般情况下,不要阻塞线程池中的线程,因为这样会导致一些无法预见的错误。来看下面的一个例子:
1 private void btnTrheadPool_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine($"****************btnThreadPool_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 4 // 设置最大线程 5 ThreadPool.SetMaxThreads(16, 16); 6 // 设置最小线程 7 ThreadPool.SetMinThreads(8, 8); 8 ManualResetEvent manualResetEvent = new ManualResetEvent(false); 9 for (int i = 0; i < 20; i++) 10 { 11 int k = i; 12 ThreadPool.QueueUserWorkItem(p => 13 { 14 Console.WriteLine(k); 15 if (k < 18) 16 { 17 manualResetEvent.WaitOne(); 18 } 19 else 20 { 21 // 设为true 22 manualResetEvent.Set(); 23 } 24 }); 25 } 26 if (manualResetEvent.WaitOne()) 27 { 28 Console.WriteLine("没有死锁、、、"); 29 } 30 Console.WriteLine("等着QueueUserWorkItem完成后才执行"); 31 Console.WriteLine($"****************btnThreadPool_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 32 }
启动20个线程,如果k小于18就阻塞当前的线程,结果:
从截图中看出,只执行了16个线程,后面的线程没有执行,这是为什么呢?因为我们在上面设置了线程池中最多可以有16个线程,当16个线程都阻塞的时候,会造成死锁,所以后面的线程不会再执行了。
6、回调
让我们先来了解一下究竟什么是回调。回调就是启动子线程计算,子线程完成委托以后,该线程在去执行后续回调委托。在Thread和ThreadPool中其实是没有回调的,但是我们可以使用委托实现。
先定义委托和回调需要执行的方法:
1 private void DoSomething() 2 { 3 Console.WriteLine($"这是委托执行 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} "); 4 } 5 6 private void DoSomethingCallback() 7 { 8 Console.WriteLine($"这是回调执行 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} "); 9 }
定义实现回调的方法:
1 private void ThreadWithCallback(Action act,Action callback) 2 { 3 Thread thread = new Thread(() => { 4 // 执行委托 5 act.Invoke(); 6 // 执行回调 7 callback.Invoke(); 8 }); 9 // 启动线程 10 thread.Start(); 11 }
在按钮里面调用该方法:
1 private void btnTrheadPool_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine($"****************btnThreadPool_Click Start 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 4 Action action = this.DoSomething; 5 Action actCallback = this.DoSomethingCallback; 6 ThreadWithCallback(action, actCallback); 7 Console.WriteLine($"****************btnThreadPool_Click End 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 8 }
结果:
从截图中可以看出,DoSomethingCallback回调方法是在DoSomething方法执行完毕以后才执行的。
7、线程重用
ThreadPool可以很好的实现线程的重用:
1 private void btnTrheadPool_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine($"****************btnThreadPool_Click Start 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 4 // 线程重用 5 ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click")); 6 ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click")); 7 ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click")); 8 ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click")); 9 ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click")); 10 Thread.Sleep(10 * 1000); 11 Console.WriteLine("前面的计算都完成了。。。。。。。。"); 12 ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click")); 13 ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click")); 14 ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click")); 15 ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click")); 16 ThreadPool.QueueUserWorkItem(t => this.DoSomethingLong("btnThreadPool_Click")); 17 Console.WriteLine($"****************btnThreadPool_Click End 当前线程:{Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 18 }
结果:
可以看出ThreadPool可以实现线程的重用。
Thread不能实现线程的重用:
1 private void Threads_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine($"****************btnThreads_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 4 for (int i = 0; i < 5; i++) 5 { 6 new Thread(() => this.DoSomethingLong("btnThreads_Click")).Start(); 7 } 8 Thread.Sleep(10 * 1000); 9 Console.WriteLine("前面的计算都完成了。。。。。。。。"); 10 for (int i = 0; i < 5; i++) 11 { 12 new Thread(() => this.DoSomethingLong("btnThreads_Click")).Start(); 13 } 14 Console.WriteLine($"****************btnThreads_Click End {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************"); 15 }
结果:
可以看出前后两次的线程都是不一样的,所以Thread不能实现线程的重用。