通过实验研究“线程池中线程数目的变化规律”
自从看了老赵关于线程池的实验以后,我就想学着做一个类似的实验,验证自己的理解,现在终于做好了,请大家指正。
一般情况下我们都使用Thread类创建线程,因为通过Thread对象可以对线程进行灵活的控制。但创建线程和销毁线程代价不菲,过多的线程会消耗掉大量的内存和CPU资源,假如某段时间内突然爆发了100个短小的线程,创建和销毁这些线程就会消耗很多时间,可能比线程本身运行的时间还长。为了改善这种状况,.NET提供了一种称之为线程池(Thread Pool)的技术。线程池提供若干个固定线程轮流为大量的任务服务,比如用10个线程轮流执行100个任务,当一个线程完成任务时,并不马上销毁,而是接手另一个任务,从而减少创建和销毁线程的消耗。
一、SetMaxThreads()的作用
MSDN里这样解释:“检索可以同时处于活动状态的线程池请求的数目。所有大于此数目的请求将保持排队状态,直到线程池线程变为可用。”。这句话可真够拗口,按我的理解,就是设置线程池中所能容纳的最大线程数目,当任务数大于这个最大值时,多余的任务将在线程池外排队等候。
下面我们做个试验来验证一下,向一个线程池中排入50个线程,观察活动线程数(正在执行任务的线程,不包括空闲线程)的变化情况。
using System; using System.Collections.Generic; using System.Text; using System.Threading; using System.Diagnostics; namespace ThreadPoolTest { class Program { public static void ThreadPoolTest() { ThreadPool.SetMaxThreads(20, 20); //线程数目上限设置为20 ThreadPool.SetMinThreads(10, 10); //线程数目下限设置为10 Console.WriteLine("活动线程数\t" + "线程名称\t" + "状态"); //向线程池中添加50个工作线程 for (int i = 1; i <= 50; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback(WorkFunction), i); } } private static object Lock = new object(); //工作函数 public static void WorkFunction(object n) { lock (Lock) { Console.WriteLine(GetNumberOfRuningThreads() + "\t\tTask" + n + "\t\tstarted"); } //Do something Thread.Sleep(18681); lock (Lock) { Console.WriteLine(GetNumberOfRuningThreads() + "\t\tTask" + n + "\t\tfinished"); } } //获取正在执行任务的线程数 public static int GetNumberOfRuningThreads() { int max1, max2; ThreadPool.GetMaxThreads(out max1, out max2); int available1, available2; ThreadPool.GetAvailableThreads(out available1, out available2); return (max1 - available1); } static void Main(string[] args) { ThreadPoolTest(); Console.ReadKey(); //按下任意键结束程序 } } }
运行结果如下:
活动线程数 线程名称 状态
1 Task1 started
2 Task2 started
3 Task3 started
4 Task4 started
5 Task5 started
6 Task6 started
7 Task7 started
8 Task8 started
9 Task9 started
10 Task10 started
11 Task11 started
12 Task12 started
13 Task13 started
14 Task14 started
15 Task15 started
16 Task16 started
17 Task17 started
18 Task18 started
19 Task19 started
20 Task20 started
20 Task1 finished
20 Task21 started
20 Task3 finished
20 Task22 started
20 Task2 finished
20 Task23 started
20 Task4 finished
20 Task24 started
20 Task6 finished
20 Task25 started
20 Task8 finished
20 Task26 started
20 Task5 finished
20 Task27 started
20 Task9 finished
20 Task28 started
20 Task7 finished
20 Task29 started
20 Task10 finished
20 Task30 started
20 Task11 finished
20 Task31 started
20 Task12 finished
20 Task32 started
20 Task13 finished
20 Task33 started
20 Task14 finished
20 Task34 started
20 Task15 finished
20 Task35 started
20 Task16 finished
20 Task36 started
20 Task17 finished
20 Task37 started
20 Task18 finished
20 Task38 started
20 Task19 finished
20 Task39 started
20 Task20 finished
20 Task40 started
20 Task21 finished
20 Task41 started
20 Task22 finished
20 Task42 started
20 Task23 finished
20 Task43 started
20 Task24 finished
20 Task44 started
20 Task25 finished
20 Task45 started
20 Task26 finished
20 Task46 started
20 Task28 finished
20 Task47 started
20 Task29 finished
20 Task48 started
20 Task27 finished
20 Task49 started
20 Task30 finished
20 Task50 started
20 Task31 finished
19 Task32 finished
18 Task33 finished
17 Task34 finished
16 Task35 finished
15 Task36 finished
14 Task37 finished
13 Task38 finished
12 Task39 finished
11 Task40 finished
10 Task41 finished
9 Task42 finished
8 Task43 finished
7 Task44 finished
6 Task45 finished
5 Task46 finished
4 Task47 finished
3 Task48 finished
2 Task49 finished
1 Task50 finished
(没想到数据这么完美,运气挺好的)
从上面的结果可以看出,线程的随着任务的增加,活动线程的数目也逐步增加,直到达到上限值20为止,之后,活动线程的数目保持不变,多余的任务在线程池外排队,当有线程完成任务时,就从排队等候的线程中取一个来执行,因此虽然任务数在变化,但线程数目保持不变。随着任务的不断完成,某一时刻,任务数少于20个,此时活动线程也开始逐步减少。
二、SetMinThreads()的作用
MSDN中这样解释:“检索线程池在新请求预测中维护的空闲线程数。”
这句话我也挺“专业”的,不太好懂。我的理解是:设置线程池中线程数目的下限,当任务小于下限时,不足的用空线程补足。
如何才能验证这一点呢?我首先想到的是察看总线程数目的变化情况(包括执行任务的和空闲的线程),如果总线程的最小值是MinThreads,就说明我们的猜测是正确的。但我找了半天,ThreadPool类并没有提供这样的接口,所以我们需要另想办法。
我的办法是观察线程的创建的时间。按照设想,线程池中本来就有若干空线程,所以一开始线程开始的时间非常快。当空线程用完时,也就是当任务数量会超出线程数量时,线程池并不会立即创建新线程,而是等待大约500毫秒左右,这么做的目的是看看在这段时间内是否有其他线程完成任务来接手这个请求,这样就可以避免因创建新线程而造成的消耗。所以,后面的线程创建速度应该比较慢。
总之一开始,创建线程的速度应该是很快的,当线程数超过下限值时,创建速度就会慢下来。
using System; using System.Collections.Generic; using System.Text; using System.Threading; using System.Diagnostics; namespace ThreadPoolTest { class Program { public static Stopwatch watch = new Stopwatch(); public static void ThreadPoolTest() { watch.Start(); ThreadPool.SetMaxThreads(20, 20); //线程数目上限设置为20 ThreadPool.SetMinThreads(10, 10); //线程数目下限设置为10 Console.WriteLine("时间\t\t活动线程数\t" + "线程名称\t" + "状态"); //向线程池中添加50个工作线程 for (int i = 1; i <= 50; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback(WorkFunction), i); } } private static object Lock = new object(); //工作函数 public static void WorkFunction(object n) { lock (Lock) { Console.WriteLine(watch.Elapsed + "\t" + GetNumberOfRuningThreads() + "\t\tTask" + n + "\t\tstarted"); } //Do something Thread.Sleep(38681); lock (Lock) { Console.WriteLine(watch.Elapsed + "\t" + GetNumberOfRuningThreads() + "\t\tTask" + n + "\t\tfinished"); } } //获取正在执行任务的线程数 public static int GetNumberOfRuningThreads() { int max1, max2; ThreadPool.GetMaxThreads(out max1, out max2); int available1, available2; ThreadPool.GetAvailableThreads(out available1, out available2); return (max1 - available1); } static void Main(string[] args) { ThreadPoolTest(); Console.ReadKey(); //按下任意键结束程序 } } }
运行结果如下:
时间 活动线程数 线程名称 状态
00:00:00.0107238 1 Task1 started
00:00:00.0140175 3 Task3 started
00:00:00.0151379 7 Task7 started
00:00:00.0160629 8 Task2 started
00:00:00.0165277 8 Task8 started
00:00:00.0167671 9 Task5 started
00:00:00.0169506 10 Task9 started
00:00:00.0170967 10 Task4 started
00:00:00.0172783 10 Task10 started
00:00:00.0174646 10 Task6 started
00:00:01.0162788 11 Task11 started
00:00:02.0156464 12 Task12 started
00:00:03.0152283 13 Task13 started
00:00:04.0146323 14 Task14 started
00:00:05.0141607 15 Task15 started
00:00:06.0136234 16 Task16 started
00:00:07.0640600 17 Task17 started
00:00:08.0624471 18 Task18 started
00:00:09.0621401 19 Task19 started
00:00:10.0619472 20 Task20 started
00:00:38.7566261 20 Task1 finished
00:00:38.7576175 20 Task3 finished
00:00:38.7586037 20 Task22 started
00:00:38.7589606 20 Task10 finished
00:00:38.7609881 20 Task23 started
00:00:38.7644790 20 Task4 finished
00:00:38.7653911 20 Task24 started
00:00:38.7661620 20 Task9 finished
00:00:38.7667461 20 Task25 started
00:00:38.7676772 20 Task21 started
00:00:38.7683277 20 Task8 finished
00:00:38.7689285 20 Task26 started
00:00:38.7693858 20 Task2 finished
00:00:38.7700532 20 Task6 finished
00:00:38.7706129 20 Task28 started
00:00:38.7711820 20 Task5 finished
00:00:38.7716580 20 Task29 started
00:00:38.7722943 20 Task27 started
00:00:38.7728069 20 Task7 finished
00:00:38.7733115 20 Task30 started
00:00:39.7583735 20 Task11 finished
00:00:39.7591793 20 Task31 started
00:00:40.7578469 20 Task12 finished
00:00:40.7593474 20 Task32 started
00:00:41.7574471 20 Task13 finished
00:00:41.7582969 20 Task33 started
00:00:42.7569026 20 Task14 finished
00:00:42.7576400 20 Task34 started
00:00:43.7563349 20 Task15 finished
00:00:43.7570931 20 Task35 started
00:00:44.7559455 20 Task16 finished
00:00:44.7567992 20 Task36 started
00:00:45.7593267 20 Task17 finished
00:00:45.7599964 20 Task37 started
00:00:46.7568787 20 Task18 finished
00:00:46.7575400 20 Task38 started
00:00:47.7562341 20 Task19 finished
00:00:47.7569503 20 Task39 started
00:00:48.7568063 20 Task20 finished
00:00:48.7574898 20 Task40 started
00:01:17.4420354 20 Task22 finished
00:01:17.4438299 20 Task41 started
00:01:17.4464655 20 Task23 finished
00:01:17.4478110 20 Task42 started
00:01:17.4488933 20 Task24 finished
00:01:17.4496002 20 Task43 started
00:01:17.4508097 20 Task21 finished
00:01:17.4513800 20 Task44 started
00:01:17.4520468 20 Task25 finished
00:01:17.4526202 20 Task45 started
00:01:17.4532567 20 Task28 finished
00:01:17.4537159 20 Task46 started
00:01:17.4546202 20 Task26 finished
00:01:17.4552586 20 Task47 started
00:01:17.4558928 20 Task27 finished
00:01:17.4564289 20 Task48 started
00:01:17.4571296 20 Task30 finished
00:01:17.4577266 20 Task49 started
00:01:17.4582889 20 Task29 finished
00:01:17.4592571 20 Task50 started
00:01:18.4418865 20 Task31 finished
00:01:19.4424421 19 Task32 finished
00:01:20.4399553 18 Task33 finished
00:01:21.4405072 17 Task34 finished
00:01:22.4400143 16 Task35 finished
00:01:23.4394151 15 Task36 finished
00:01:24.4429568 14 Task37 finished
00:01:25.4404414 13 Task38 finished
00:01:26.4399282 12 Task39 finished
00:01:27.4404093 11 Task40 finished
00:01:56.1459537 10 Task41 finished
00:01:56.1485649 9 Task42 finished
00:01:56.1505286 8 Task43 finished
00:01:56.1514803 7 Task44 finished
00:01:56.1534702 6 Task45 finished
00:01:56.1544100 5 Task46 finished
00:01:56.1552989 4 Task47 finished
00:01:56.1574599 3 Task48 finished
00:01:56.1586138 2 Task49 finished
00:01:56.1602353 1 Task50 finished
(注意,由于线程轮换的不确定性,每次结果都会稍有不同)
从结果可以看出,前十个线程很短时间内开始,从第十一个线程开始则要经过较长一段时间才会创建(大约一秒)。从第二十个线程开始,要等前面的任务完成,新任务才能开始(这里等了一段较长时间),完成一个,就开始一个,线程总数保持不变。直到任务数小于线程上限,活动线程数才开始逐渐减少。
总之,线程的运行过程可以描述如下(为了叙述方便,我们假设下限为10,上限为20):
1.当线程池被创建后,里面就会创建10个空线程(和下限值相同)。
2.当我们向线程池中排入一个任务后,就会有一个空线程接手该任务,然后运行起来。随着我们不断向线程池中排入任务,线程池中的空线程逐一接手并执行任务。
3.随着任务的不断增加,在某一时刻任务数量会超出下限,这时线程的数量就不够用了,但线程池并不会立即创建新线程,而是等待大约500毫秒左右,这么做的目的是看看在这段时间内是否有其他线程完成任务来接手这个请求,这样就可以避免因创建新线程而造成的消耗。如果这段时间内没有线程完成任务,就创建一个新线程去执行新任务。
4.在任务数量超过下限后,随着新任务的不断排入,线程池中线程数量持续增加,直至线程数量达到上限值为止。
5.当线程数量达到上限时,继续增加任务,线程数量将不再增加。比如你向线程池中排入50个任务,则只有20个进入线程池(和上限相同),另外30个在线程池外排队等待。当线程池中的某个线程完成任务后,并不会立即终止,而是从等待队列中选择一个任务继续执行,这样就减少了因创建和销毁线程而消耗的时间。
6.随着任务逐步完成,线程池外部等候的任务被逐步调入线程池,任务的数量逐步减少,但线程的数量保持恒定,始终为20(和上限值相同)。
7.随着任务被逐步完成,总有某一时刻,任务数量会小于上限值,这时线程池内多余的线程会在空闲2分钟后被释放并回收相关资源。线程数目逐步减少,直到达到下限值为止。
8.当任务数量减小到下限值之下时,线程池中的线程数目保持不变(始终和下限值相同),其中一部分在执行任务,另一部分处于空运行状态。
9.当所有任务都完成后,线程池恢复初始状态,运行10个空线程。
由上面的论述可以看出线程池提高效率的关键是一个线程完成任务后可以继续为其他任务服务,这样就可以使用有限的几个固定线程轮流为大量的任务服务,从而减少了因频繁创建和销毁线程所造成的消耗。
下面是修改Sleep()时间后再次运行的结果。
时间 活动线程数 线程名称 状态
00:00:00.0189359 9 Task2 started
00:00:00.0201952 9 Task1 started
00:00:00.0208337 9 Task9 started
00:00:00.0212512 9 Task6 started
00:00:00.0217229 9 Task8 started
00:00:00.0223773 9 Task7 started
00:00:00.0227899 9 Task4 started
00:00:00.0231066 9 Task3 started
00:00:00.0235849 9 Task5 started
00:00:01.0163201 10 Task10 started
00:00:02.0157285 11 Task11 started
00:00:03.0153552 12 Task12 started
00:00:04.0147554 13 Task13 started
00:00:05.0142889 14 Task14 started
00:00:06.0208071 15 Task15 started
00:00:06.7049884 15 Task2 finished
00:00:06.7070401 15 Task16 started
00:00:06.7074667 15 Task7 finished
00:00:06.7078560 15 Task17 started
00:00:06.7082879 15 Task3 finished
00:00:06.7086411 15 Task18 started
00:00:06.7092260 15 Task5 finished
00:00:06.7096711 15 Task19 started
00:00:06.7101447 15 Task6 finished
00:00:06.7111514 15 Task20 started
00:00:06.7146020 15 Task8 finished
00:00:06.7154948 15 Task21 started
00:00:06.7176304 15 Task9 finished
00:00:06.7193209 15 Task22 started
00:00:06.7201892 15 Task1 finished
00:00:06.7208777 15 Task23 started
00:00:06.7216695 15 Task4 finished
00:00:06.7230875 15 Task24 started
00:00:07.5200857 16 Task25 started
00:00:07.7023973 16 Task10 finished
00:00:07.7036477 16 Task26 started
00:00:08.5195702 17 Task27 started
00:00:08.7011903 17 Task11 finished
00:00:08.7025396 17 Task28 started
00:00:09.5187647 18 Task29 started
00:00:09.7008700 18 Task12 finished
00:00:09.7019751 18 Task30 started
00:00:10.5246393 19 Task31 started
00:00:10.7015599 19 Task13 finished
00:00:10.7025033 19 Task32 started
00:00:11.5184213 20 Task33 started
00:00:11.7000063 20 Task14 finished
00:00:11.7006650 20 Task34 started
00:00:12.6995112 20 Task15 finished
00:00:12.7003309 20 Task35 started
00:00:13.3848916 20 Task16 finished
00:00:13.3854166 20 Task36 started
00:00:13.3861911 20 Task17 finished
00:00:13.3865749 20 Task37 started
00:00:13.3875311 20 Task18 finished
00:00:13.3881098 20 Task38 started
00:00:13.3887310 20 Task19 finished
00:00:13.3893513 20 Task39 started
00:00:13.3920574 20 Task20 finished
00:00:13.3925907 20 Task40 started
00:00:13.3949921 20 Task21 finished
00:00:13.3956187 20 Task41 started
00:00:13.3980055 20 Task22 finished
00:00:13.3985151 20 Task42 started
00:00:13.3998460 20 Task23 finished
00:00:13.4002990 20 Task43 started
00:00:13.4022154 20 Task24 finished
00:00:13.4027421 20 Task44 started
00:00:14.1998829 20 Task25 finished
00:00:14.2003381 20 Task45 started
00:00:14.3827441 20 Task26 finished
00:00:14.3833120 20 Task46 started
00:00:15.1987473 20 Task27 finished
00:00:15.1993380 20 Task47 started
00:00:15.3824547 20 Task28 finished
00:00:15.3830691 20 Task48 started
00:00:16.1973072 20 Task29 finished
00:00:16.1980986 20 Task49 started
00:00:16.3807865 20 Task30 finished
00:00:16.3814246 20 Task50 started
00:00:17.2490713 20 Task31 finished
00:00:17.4271196 19 Task32 finished
00:00:18.2423627 18 Task33 finished
00:00:18.4271299 17 Task34 finished
00:00:19.4241792 16 Task35 finished
00:00:20.1084177 15 Task36 finished
00:00:20.1103091 14 Task37 finished
00:00:20.1113160 13 Task38 finished
00:00:20.1123464 12 Task39 finished
00:00:20.1162134 11 Task40 finished
00:00:20.1190785 10 Task41 finished
00:00:20.1220864 9 Task42 finished
00:00:20.1229967 8 Task43 finished
00:00:20.1259507 7 Task44 finished
00:00:20.9331819 6 Task45 finished
00:00:21.1069320 5 Task46 finished
00:00:21.9229240 4 Task47 finished
00:00:22.1070568 3 Task48 finished
00:00:22.9214465 2 Task49 finished
00:00:23.1049741 1 Task50 finished
抛砖引玉,还有什么更好的方法,还请高手指教。