在日常使用多线程开发的时候,一般都构造一个Thread示例,然后调用Start使之执行。如果一个线程它大部分时间花费在等待某个事件响应的发生然后才予以响应;或者如果在一定期间内重复性地大量创建线程。这些时候个人感觉利用线程池(ThreadPool)会比单纯创建线程(Thread)要好。这是由于线程池能在需要的时候把空闲的线程提取出来使用,在线程使用完毕的时候对线程回收达到对象复用的效果。这个就涉及到池的性质了。线程(Thread)很容易跟数据库连接、流、Socket套接字这部分非托管资源归在一起,但是个人认为Thread并不是非托管资源,有个低级点的判别办法,就是Thread没有去实现IDispose接口,利用Reflector打开去查看的话,里面就有一个析构函数~Thread()它实际上是调用了一个外部方法InternalFinalize(),估计这个就涉及到CLR里面的东西了。如果频繁开启线程,对资源的消耗会比用线程池的要多。
既然上面提及到池的性质,在TheardPool这个线程池中也可以看到一个对象池的特点,这个可以在日后我们创建对象池时可以作为参考。虽然本人以前也写过一个Socket的对象池,但是运行起来的性能不好。现在个人不清楚在CLR中是否本身存在一个Socket的对象池,但看了老赵的博客发现CLR内部其实拥有一个数据库连接的对象池,实现的效果跟ThreadPool类似,能让对象复用。
在以前定义Socket池时只定义了一个对象上限,没有下限的概念;在ThreadPool中,池内对象的上下限都可以进行设置和获取
1 public static bool SetMinThreads(int workerThreads, int completionPortThreads);2 public static bool SetMaxThreads(int workerThreads, int completionPortThreads);3 4 public static void GetMaxThreads(out int workerThreads, out int completionPortThreads);5 public static void GetMinThreads(out int workerThreads, out int completionPortThreads);
至于这里有两种线程的原因迟点再提。MinThread指的是线程池初始或者空闲时保留最少的线程数,这个值与CLR的版本和CPU的核心数有关系。在CLR SP1之前的版本中,线程池默认最大线程数是 处理器数 * 25,在CLR SP1之后默认最大线程数是处理器数 * 250。最少线程数则是 处理器数,于是我也尝试了一下。不过这里又涉及到CLR与.NET Framework的关系。
.NET Framework | CLR---------------------------------------2.0 RTM |2.0.50727.42 2.0 SP1 |2.0.50727.1433 2.0 SP2 |2.0.50727.3053 3.0 RTM |2.0 RTM 3.0 SP1 |2.0 SP1 3.0 SP2 |2.0 SP2 3.5 RTM |2.0 SP1 3.5 SP1 |2.0 SP2 4.0 RTM |4.0.30319.1
我自己通过 Environment类的Version属性获取CLR的版本号。下面这段代码,我使用几个版本的.NET Framework去编译 。
int i1,i2; ThreadPool.GetMaxThreads(out i1, out i2); Console.WriteLine("Max workerThreads :"+ i1+" completionPortThreads:"+i2); ThreadPool.GetMinThreads(out i1,out i2); Console.WriteLine("Min workerThreads:"+i1 + " completionPortThreads:" + i2); Console.WriteLine(" CLR Version: {0} ", Environment.Version);
得出的结果有点失望,失望的不是与上面说的相违背。而是我这里用的.NET Framework不全。
2.0和3.5的CLR都是SP2本版本的
3.5的结果如下
2.0的结果如下
从上面的结果看出最大线程数和最小线程数符合。还是得说一下我用的是i5处理器,双核四线程。
下面这个我是在虚拟机上跑的,单核的虚拟机
用的是.NET Framework1.0的,CLR也是1.0的。的确最少线程数和最多工作线程数是对得上的,但是IO线程数还是保留着1000个。最后看看上跑熟悉的.NET 4.0的
我在虚拟机和本机上分别跑过,IO线程还是一样1000没变,估计前面的公式对它不适用,但工作数还是有点怪怪的,单核的就1023条,但是在i5上的却不是1024的倍数。
使用了线程池这个对象,给人的感觉就不像是往常使用其他对象的那种方式——调用,而是类似于Web服务器的请求与响应的方式。这个理念跟我设计的Socket池有点不一样。说回线程池里面对线程的管理情况,在没有对线程池提交过任何任务请求的时候,线程池内真正开创的线程数可并不是那么多,实际上仅仅是小于等于最小的线程数。参照了老赵的代码
1 int maxCount = 18; 2 int minCount = 16; 3 ThreadPool.SetMaxThreads(maxCount, maxCount); 4 ThreadPool.SetMinThreads(minCount, minCount); 5 6 Stopwatch watch = new Stopwatch(); 7 watch.Start(); 8 9 WaitCallback callback = i =>10 {11 Console.WriteLine(String.Format("{0}: Task {1} started", watch.Elapsed, i));12 Thread.Sleep(10000);13 Console.WriteLine(String.Format("{0}: Task {1} finished", watch.Elapsed, i));14 };15 16 for (int i = 0; i < 20; i++)17 {18 ThreadPool.QueueUserWorkItem(callback, i);19 }
运行结果如下
从上图可以看出,当一开始请求任务的时候,线程池能马上响应去处理任务,16条信息都能在一秒内完成,而这个16则是刚与最小线程数相等。而老赵的博客上说一秒内创建的线程数会小于最小线程数。估计是我现在用的处理器性能还可以吧。不过我也在单核的虚拟机上运行,同样也是一秒内创建的线程数跟最小线程数相等。但同时我也发现了另一个情况,就是在真实的电脑上运行上述代码,把最小线程数设成小于4的,同样一开始也能同时创建了4条线程,个人估计这个跟具有双核四线程的i5CPU有很大关系,在虚拟机上运行就没这情况了。
既然初始创建的线程数并非是最大线程数,而是在线程池使用过程中遇到线程不够用了才去创建新线程,直到达到最大值为止,这样的设计大大节省了对资源的占用。同时也引发了另一个问题,线程的创建速度,这个创建速度会影响到响应请求的时间。每次请求肯定希望尽快得到响应,但是如果响应的速度过快,万一在一瞬间有大量简短的任务涌入线程池,任务完毕后对已经用完的线程进行回收也是一个比较大的开销。所以这个线程的创建速度也是得讲究的。看了并运行过老赵的代码,的确发现1秒内会创建了两个线程,但绝大部分是1秒只创建一个。我自己稍作改动,让结果更清晰些
1 Dictionary<int, TimeSpan> createTime = new Dictionary<int, TimeSpan>(); 2 int maxCount = 12; 3 int minCount = 5; 4 ThreadPool.SetMaxThreads(maxCount, maxCount); 5 ThreadPool.SetMinThreads(minCount, minCount); 6 7 Stopwatch watch = new Stopwatch(); 8 watch.Start(); 9 10 WaitCallback callback = i =>11 {12 lock (this)13 {14 TimeSpan ts = watch.Elapsed;15 if (!createTime.ContainsKey(Thread.CurrentThread.ManagedThreadId))16 {17 createTime[Thread.CurrentThread.ManagedThreadId] = ts;18 Console.WriteLine("{0} {1} {2}", Thread.CurrentThread.ManagedThreadId, ts, i);19 }20 }21 Thread.Sleep(10000);22 };23 24 for (int i = 0; i < 20; i++)25 {26 ThreadPool.QueueUserWorkItem(callback, i);27 }28
同样运行老赵的代码也不一定能看到每秒创建两个线程,我段代码貌似更难以看见了,估计是因为有了锁的原因。
这个结果我试了很多回才弄了出来,好像例子很生硬,但1秒一个线程还是很明显能看出来的。
在提及获取和设置线程池上下限的部分提及过,一个线程池内有两种类型的线程,一种是工作线程,另一种是IO线程。两种线程其使用时会有差异,在向线程池发出任务请求的时候,即调用QueueUserWorkItem或者UnsafeQueueUserWorkItem方法时。使用的线程是工作线程的线程。在使用APM模式时,有部分是使用了工作线程,有部分是使用了IO线程。这里大部分都是使用了工作线程,只有少部分会使用IO线程。在使用真正的异步方法回调时才会使用IO线程,哪些类的BeginXXX/EndXX方法会真正地用上异步,在鄙人上一篇博文中提到。不过本人阅读了老赵的博客反复试验之后得出了一个结果,即使是FileStream,Dns,Socket,WebRequest,SqlCommanddeng的异步操作,它们也会调用到线程池里面的线程。在不同的阶段调用了不同的线程。那么先看一下下面的代码,要注意一下的是,本人发现如果要把线程池的上下限设成同一个值的话,那只能先设下限再设上限,否则上限会恢复到默认值的。
1 ThreadPool.SetMinThreads(5, 3); 2 ThreadPool.SetMaxThreads(5, 3); 3 ManualResetEvent waitHandle = new ManualResetEvent(false); 4 5 for (int i = 0; i < 5; i++) 6 { 7 FileStream fs = new FileStream("test" + i + ".txt", FileMode.Create, Fileaccess.Write, FileShare.Write, 1024, FileOptions.Asynchronous); 8 string content = "hello world"; 9 byte[] arr = Encoding.Default.GetBytes(content);10 11 fs.BeginWrite(arr, 0, arr.Length, (asyncPara) =>12 {13 FileStream caller = asyncPara.AsyncState as FileStream;14 caller.EndWrite(asyncPara);15 caller.Close();16 caller.Dispose();17 int workC, ioC;18 ThreadPool.GetAvailableThreads(out workC,
新闻热点
疑难解答