在多线程编程中,线程的创建和销毁是非常消耗系统资源的,因此,C#引入了池的概念,类似的还有数据库连接池,这样,维护一个池,池内维护的一些线程,需要的时候从池中取出来,不需要的时候放回去,这样就避免了重复创建和销毁线程。
ThreadPool类 MSDN帮助信息
将任务添加进线程池:
ThreadPool.QueueUserWorkItem(new WaitCallback((方法名));ThreadPool.QueueUserWorkItem(new WaitCallback((方法名),传入方法的参数);
对线程池的线程数量进行控制
SetMaxThreads(Int32, Int32) //设置可以同时处于活动状态的线程池的请求数目。所有大于此数目的请求将保持排队状态,直到线程池线程变为可用。 SetMinThreads(Int32, Int32) //发出新的请求时,在切换到管理线程创建和销毁的算法之前设置线程池按需创建的线程的最小数量。
对线程池线程数量控制的验证
public static void Main() { ThreadPool.SetMinThreads(1, 1); ThreadPool.SetMaxThreads(1, 1); for (int i = 0; i < 10; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("当前值为:" + obj + "线程ID" + Thread.CurrentThread.ManagedThreadId); }),i); } Console.Read(); }
最大线程数量和最小线程数量全部设置为1,上述代码的执行结果为:
可以看到只开启了一个线程。将最大线程改为2
public static void Main() { ThreadPool.SetMinThreads(1, 1); ThreadPool.SetMaxThreads(2, 2); for (int i = 0; i < 10; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("当前值为:" + obj + "线程ID" + Thread.CurrentThread.ManagedThreadId); }),i); } Console.Read(); }
此时启动了两个线程
但是这最多和最少并不是说一定要使用这么多线程的,比如,我设置最少10个线程,但是实际上可能只试用了3-4个,但是线程池中确实是最少会维护着10个线程,不一定每次全部都启用的。
public static void Main() { ThreadPool.SetMinThreads(10, 10); ThreadPool.SetMaxThreads(20, 20); Console.WriteLine("测试开始!"); for (int i = 0; i < 10; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("当前值为:" + obj + "线程ID" + Thread.CurrentThread.ManagedThreadId); }),i); } Console.WriteLine("测试结束!"); Console.Read(); }
上面的执行结果:
为什么打印测试结束的语句执行的这么靠前呢?这是什么原因呢?
这是因为在循环中将任务添加到线程池中后,并没有等待线程执行完成再继续执行主线程,也就是线程池中的现成是如何启动及结束我们是不知道的,ThreadPool没有提供简单的方法来获取工作线程是否已经结束,所以需要通过其他方法实现。此时,需要引入ManualResetEvent类,MSDN:,
ManualResetEvent 允许线程通过发信号互相通信。通常,此通信涉及一个线程在其他线程进行之前必须完成的任务。
当一个线程开始一个活动(此活动必须完成后,其他线程才能开始)时,它调用 Reset 以将 ManualResetEvent 置于非终止状态。此线程可被视为控制 ManualResetEvent。调用 ManualResetEvent 上的 WaitOne 的线程将阻止,并等待信号。当控制线程完成活动时,它调用 Set 以发出等待线程可以继续进行的信号。并释放所有等待线程。
一旦它被终止,ManualResetEvent 将保持终止状态,直到它被手动重置。即对 WaitOne 的调用将立即返回。
可以通过将布尔值传递给构造函数来控制 ManualResetEvent 的初始状态,如果初始状态处于终止状态,为 true;否则为 false。
方法WaitOne(Timeout.Infinite, true); 阻止当前线程,直到当前 WaitHandle 收到信号为止。
方法Set(); 将事件状态设置为终止状态,允许一个或多个等待线程继续。
这段话到底是什么意思呢?我们通过一段代码来理解
public static void Main(){ ThreadPool.SetMinThreads(3,3); ManualResetEvent mre = new ManualResetEvent(false); for (int i = 0; i < 3; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("线程 " + obj.ToString() + " 已启动! 线程ID为:" + Thread.CurrentThread.ManagedThreadId ); mre.WaitOne(); Thread.Sleep(500); Console.WriteLine("线程 " + obj.ToString() + " 已结束!线程ID为:"
+ Thread.CurrentThread.ManagedThreadId ); }), i); } Console.Read();}
上述代码的执行结果是:
从执行结果中可以看到,我们往线程池中添加了三个任务,线程池启用了三个线程去执行。当任务方法执行到mre.WaitOne();时,线程被ManualResetEvent 阻止,并没有继续往下走,也就是此时线程们正在等待一个信号,下面我们就给线程们发出这个信号。
public static void Main(){ ThreadPool.SetMinThreads(3,3); ManualResetEvent mre = new ManualResetEvent(false); for (int i = 0; i < 3; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("线程 " + obj.ToString() + " 已启动! 线程ID为:" + Thread.CurrentThread.ManagedThreadId ); mre.WaitOne(); Thread.Sleep(500); Console.WriteLine("线程 " + obj.ToString() + " 已结束!线程ID为:" + Thread.CurrentThread.ManagedThreadId); }), i); } if (Console.ReadLine() == "go") { mre.Set(); } Console.Read();}
三个新线程虽然被阻止,但是主线程是可以继续执行的,当主线程收到用户输入的go命令时,给三个线程发送信号,线程们收到信号后继续执行,并打印出执行结束的标识。
相信到这里我们应该能够理解WaitOne和Set的用法了,下面我们在看看Reset方法,我们在mre.Set()后,在开启三个新的线程
public static void Main(){ ThreadPool.SetMinThreads(3,3); ManualResetEvent mre = new ManualResetEvent(false); for (int i = 0; i < 3; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("线程 " + obj.ToString() + " 已启动! 线程ID为:" + Thread.CurrentThread.ManagedThreadId ); mre.WaitOne(); Thread.Sleep(600); Console.WriteLine("线程 " + obj.ToString() + " 已结束!线程ID为:" + Thread.CurrentThread.ManagedThreadId); }), i); } if (Console.ReadLine() == "go") { mre.Set(); } Thread.Sleep(1000); Console.WriteLine("*******************************再开启三个线程***********************************"); for (int i = 3; i < 6; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("线程 " + obj.ToString() + " 已启动! 线程ID为:" + Thread.CurrentThread.ManagedThreadId); mre.WaitOne(); Thread.Sleep(600); Console.WriteLine("线程 " + obj.ToString() + " 已结束!线程ID为:" + Thread.CurrentThread.ManagedThreadId); }), i); } Console.Read();}
运行代码看看效果
新开的线程和之前开的线程的任务方法是一模一样的啊,为什么没有等待信号而直接继续运行了呢?
这是因为我们在调用ManualResetEvent的Set方法后,在调用其 方法前会一直保持终止状态,所以,新线程任务方法中的WaitOne是无效的,因为此时ManualResetEvent是终止状态的。下面我们加上Reset方法看看效果
public static void Main(){ ThreadPool.SetMinThreads(3,3); ManualResetEvent mre = new ManualResetEvent(false); for (int i = 0; i < 3; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("线程 " + obj.ToString() + " 已启动! 线程ID为:" + Thread.CurrentThread.ManagedThreadId ); mre.WaitOne(); Thread.Sleep(600); Console.WriteLine("线程 " + obj.ToString() + " 已结束!线程ID为:" + Thread.CurrentThread.ManagedThreadId); }), i); } if (Console.ReadLine() == "go") { mre.Set(); } Thread.Sleep(1000); Console.WriteLine("*******************************再开启三个线程***********************************"); mre.Reset(); for (int i = 3; i < 6; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback((object obj) => { Console.WriteLine("线程 " + obj.ToString() + " 已启动! 线程ID为:" + Thread.CurrentThread.ManagedThreadId); mre.WaitOne(); Thread.Sleep(600); Console.WriteLine("线程 " + obj.ToString() + " 已结束!线程ID为:" + Thread.CurrentThread.ManagedThreadId); }), i); } if (Console.ReadLine() == "go") { mre.Set(); } Console.Read();}
执行结果:
此时,达到了我们想要的结果。
最后调用 mre.Close();释放资源即可。
现在明白了ManualResetEvent类的使用,想要解决开始的“测试开始、测试结束”打印顺序问题就是张飞吃豆芽了吧?!