线程池ThreadPool的初探
一、线程池的适用范围
在日常使用多线程开发的时候,一般都构造一个Thread示例,然后调用Start使之执行。如果一个线程它大部分时间花费在等待某个事件响应的发生然后才予以响应;或者如果在一定期间内重复性地大量创建线程。这些时候个人感觉利用线程池(ThreadPool)会比单纯创建线程(Thread)要好。这是由于线程池能在需要的时候把空闲的线程提取出来使用,在线程使用完毕的时候对线程回收达到对象复用的效果。这个就涉及到池的性质了。线程(Thread)很容易跟数据库连接、流、Socket套接字这部分非托管资源归在一起,但是个人认为Thread并不是非托管资源,有个低级点的判别办法,就是Thread没有去实现IDispose接口,利用Reflector打开去查看的话,里面就有一个析构函数~Thread()它实际上是调用了一个外部方法InternalFinalize(),估计这个就涉及到CLR里面的东西了。如果频繁开启线程,对资源的消耗会比用线程池的要多。
二、池的容量和对象管理
既然上面提及到池的性质,在TheardPool这个线程池中也可以看到一个对象池的特点,这个可以在日后我们创建对象池时可以作为参考。虽然本人以前也写过一个Socket的对象池,但是运行起来的性能不好。现在个人不清楚在CLR中是否本身存在一个Socket的对象池,但看了老赵的博客发现CLR内部其实拥有一个数据库连接的对象池,实现的效果跟ThreadPool类似,能让对象复用。
在以前定义Socket池时只定义了一个对象上限,没有下限的概念;在ThreadPool中,池内对象的上下限都可以进行设置和获取
public static bool SetMinThreads(int workerThreads, int completionPortThreads);
public static bool SetMaxThreads(int workerThreads, int completionPortThreads); public static void GetMaxThreads(out int workerThreads, out int completionPortThreads);
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池有点不一样。说回线程池里面对线程的管理情况,在没有对线程池提交过任何任务请求的时候,线程池内真正开创的线程数可并不是那么多,实际上仅仅是小于等于最小的线程数。参照了老赵的代码
int maxCount = ;
int minCount = ;
ThreadPool.SetMaxThreads(maxCount, maxCount);
ThreadPool.SetMinThreads(minCount, minCount); Stopwatch watch = new Stopwatch();
watch.Start(); WaitCallback callback = i =>
{
Console.WriteLine(String.Format("{0}: Task {1} started", watch.Elapsed, i));
Thread.Sleep();
Console.WriteLine(String.Format("{0}: Task {1} finished", watch.Elapsed, i));
}; for (int i = ; i < ; i++)
{
ThreadPool.QueueUserWorkItem(callback, i);
}
运行结果如下
从上图可以看出,当一开始请求任务的时候,线程池能马上响应去处理任务,16条信息都能在一秒内完成,而这个16则是刚与最小线程数相等。而老赵的博客上说一秒内创建的线程数会小于最小线程数。估计是我现在用的处理器性能还可以吧。不过我也在单核的虚拟机上运行,同样也是一秒内创建的线程数跟最小线程数相等。但同时我也发现了另一个情况,就是在真实的电脑上运行上述代码,把最小线程数设成小于4的,同样一开始也能同时创建了4条线程,个人估计这个跟具有双核四线程的i5CPU有很大关系,在虚拟机上运行就没这情况了。
既然初始创建的线程数并非是最大线程数,而是在线程池使用过程中遇到线程不够用了才去创建新线程,直到达到最大值为止,这样的设计大大节省了对资源的占用。同时也引发了另一个问题,线程的创建速度,这个创建速度会影响到响应请求的时间。每次请求肯定希望尽快得到响应,但是如果响应的速度过快,万一在一瞬间有大量简短的任务涌入线程池,任务完毕后对已经用完的线程进行回收也是一个比较大的开销。所以这个线程的创建速度也是得讲究的。看了并运行过老赵的代码,的确发现1秒内会创建了两个线程,但绝大部分是1秒只创建一个。我自己稍作改动,让结果更清晰些
Dictionary<int, TimeSpan> createTime = new Dictionary<int, TimeSpan>();
int maxCount = ;
int minCount = ;
ThreadPool.SetMaxThreads(maxCount, maxCount);
ThreadPool.SetMinThreads(minCount, minCount); Stopwatch watch = new Stopwatch();
watch.Start(); WaitCallback callback = i =>
{
lock (this)
{
TimeSpan ts = watch.Elapsed;
if (!createTime.ContainsKey(Thread.CurrentThread.ManagedThreadId))
{
createTime[Thread.CurrentThread.ManagedThreadId] = ts;
Console.WriteLine("{0} {1} {2}", Thread.CurrentThread.ManagedThreadId, ts, i);
}
}
Thread.Sleep();
}; for (int i = ; i < ; i++)
{
ThreadPool.QueueUserWorkItem(callback, i);
}
同样运行老赵的代码也不一定能看到每秒创建两个线程,我段代码貌似更难以看见了,估计是因为有了锁的原因。
这个结果我试了很多回才弄了出来,好像例子很生硬,但1秒一个线程还是很明显能看出来的。
三、池内对象分类
在提及获取和设置线程池上下限的部分提及过,一个线程池内有两种类型的线程,一种是工作线程,另一种是IO线程。两种线程其使用时会有差异,在向线程池发出任务请求的时候,即调用QueueUserWorkItem或者UnsafeQueueUserWorkItem方法时。使用的线程是工作线程的线程。在使用APM模式时,有部分是使用了工作线程,有部分是使用了IO线程。这里大部分都是使用了工作线程,只有少部分会使用IO线程。在使用真正的异步方法回调时才会使用IO线程,哪些类的BeginXXX/EndXX方法会真正地用上异步,在鄙人上一篇博文中提到。不过本人阅读了老赵的博客反复试验之后得出了一个结果,即使是FileStream,Dns,Socket,WebRequest,SqlCommanddeng的异步操作,它们也会调用到线程池里面的线程。在不同的阶段调用了不同的线程。那么先看一下下面的代码,要注意一下的是,本人发现如果要把线程池的上下限设成同一个值的话,那只能先设下限再设上限,否则上限会恢复到默认值的。
ThreadPool.SetMinThreads(, );
ThreadPool.SetMaxThreads(, );
ManualResetEvent waitHandle = new ManualResetEvent(false); for (int i = ; i < ; i++)
{
FileStream fs = new FileStream("test" + i + ".txt", FileMode.Create, FileAccess.Write, FileShare.Write, , FileOptions.Asynchronous);
string content = "hello world";
byte[] arr = Encoding.Default.GetBytes(content); fs.BeginWrite(arr, , arr.Length, (asyncPara) =>
{
FileStream caller = asyncPara.AsyncState as FileStream;
caller.EndWrite(asyncPara);
caller.Close();
caller.Dispose();
int workC, ioC;
ThreadPool.GetAvailableThreads(out workC, out ioC);
Console.WriteLine(String.Format("Write Finish work {0} io {1}", workC, ioC));
waitHandle.WaitOne();
}, fs);
}
这里运用到了线程池ThreadPool的GetAvailableThreads方法,方法的描述是获取线程池最大线程数和当前使用线程数的差值,个人认为就是获取线程池的空闲线程数。在前面的文章中已经提及到,异步方法回调时会开辟线程回调方法,而这条开辟的线程是来自于线程池的,这段代码中只调用了FileStream类的异步方法,并没有其他调用线程池的方法。看看运行结果
可以明显地看出,在异步写文件的操作中,工作线程有被使用,IO线程也有被使用,这个结果跟我之前猜测的有出入。原本以为进行异步操作时,调用了Begin方法则是利用了系统API去让设备直接访存,在DMA结束之后开辟了一条IO线程去进行方法的回调。但是看了这个情况之后,本人就认为,在调用了Begin方法之后,线程池使用了一条IO线程去调用系统的API让设备访存,结束后使用的线程却是一条工作线程。假如这时候工作线程已经用完了,那么对于调用“真异步”或者“假异步”的线程来说都会造成阻塞。当然这里用了回调函数,那么不用回调函数的结果会怎么样。把工作线程设成只有一条。
ThreadPool.SetMinThreads(, );
ThreadPool.SetMaxThreads(, ); ManualResetEvent waitHandle = new ManualResetEvent(false);
for (int i = ; i < ; i++)
ThreadPool.QueueUserWorkItem((para) =>
{
waitHandle.WaitOne();
});
string content = "hello world";
content += "end";
byte[] arr = Encoding.Default.GetBytes(content); for (int i = ; i < ; i++)
{
FileStream fs = new FileStream("test" + i + ".txt", FileMode.Create, FileAccess.Write, FileShare.Write, , FileOptions.Asynchronous); IAsyncResult result = fs.BeginWrite(arr, , arr.Length, null, null); fs.EndWrite(result);
fs.Close();
fs.Dispose();
}
文件照样能正常输出,没有因工作线程已经用完而影响。但IO线程与工作线程两者间并非没有联系,在老赵的博客中看到,如果工作线程已经用完,而调用WebRequest的BeginGetResponse异步方法则会抛出一个InvalidOperationException异常,ThreadPool 中没有足够的自由线程来完成该操作。但不一定所有异步操作都会有这问题,就像上面的FileStream一样。
四、不适用线程池的场景
根据上面试验得出的结果,再加上本文一开始介绍了ThreadPool适用的场景,现在也说说线程池不适用的场景。如果要调整线程的优先级的话,还是自己开线程吧!线程池内的所有线程都是默认Normal优先级的。如果任务执行的时间比较长的话,建议还是自己开线程,因为有可能阻塞了线程池里面的线程最终导致线程池的线程被耗光。如果任务是要马上执行的,建议还是使用线程池,因为往线程池提交的任务都需要排队,线程池建立新线程的速度不多于1秒两个。
最后附上老赵三篇博客的连接,各位觉得在下有什么说错的欢迎批评指正,有什么建议或意见尽管说说。谢谢!
线程池ThreadPool的初探的更多相关文章
- C#多线程学习 之 线程池[ThreadPool](转)
在多线程的程序中,经常会出现两种情况: 一种情况: 应用程序中,线程把大部分的时间花费在等待状态,等待某个事件发生,然后才能给予响应 这一般使用ThreadPo ...
- 高效线程池(threadpool)的实现
高效线程池(threadpool)的实现 Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线 ...
- 多线程系列 线程池ThreadPool
上一篇文章我们总结了多线程最基础的知识点Thread,我们知道了如何开启一个新的异步线程去做一些事情.可是当我们要开启很多线程的时候,如果仍然使用Thread我们需要去管理每一个线程的启动,挂起和终止 ...
- C# -- 使用线程池 ThreadPool 执行多线程任务
C# -- 使用线程池 ThreadPool 执行多线程任务 1. 使用线程池 class Program { static void Main(string[] args) { WaitCallba ...
- 多线程Thread,线程池ThreadPool
首先我们先增加一个公用方法DoSomethingLong(string name),这个方法下面的举例中都有可能用到 #region Private Method /// <summary> ...
- C# 线程池ThreadPool的用法简析
https://blog.csdn.net/smooth_tailor/article/details/52460566 什么是线程池?为什么要用线程池?怎么用线程池? 1. 什么是线程池? .NET ...
- 多线程系列(2)线程池ThreadPool
上一篇文章我们总结了多线程最基础的知识点Thread,我们知道了如何开启一个新的异步线程去做一些事情.可是当我们要开启很多线程的时候,如果仍然使用Thread我们需要去管理每一个线程的启动,挂起和终止 ...
- C#多线程学习 之 线程池[ThreadPool]
在多线程的程序中,经常会出现两种情况: 一种情况: 应用程序中,线程把大部分的时间花费在等待状态,等待某个事件发生,然后才能给予响应 这一般使用ThreadPo ...
- 线程池ThreadPool的常用方法介绍
线程池ThreadPool的常用方法介绍 如果您理解了线程池目的及优点后,让我们温故下线程池的常用的几个方法: 1. public static Boolean QueueUserWorkItem(W ...
随机推荐
- GLFW初体验
GLFW - 很遗憾,没有找到FW的确切含义,Wiki上没有,GLFW主页也没有.猜测F表示for,W表示Window GLFW是干啥用的? 一个轻量级的,开源的,跨平台的library.支持Open ...
- How to fix updating ubuntu apt-get problem
It's my new PC with a new os of ubuntu. every time when I want to install software or update apt-get ...
- 浏览器 的 session 如何保持?!
http://qindingsky.blog.163.com/blog/static/3122336200832853116360/ 在谈论session机制的时候,常常听到这样一种误解“只要关闭浏览 ...
- 用CSS实现居中的方式
直接放链接吧,最近大量时间放在看书上了,不想玩游戏,不想看电影,只想看书,早日做出自己的网站卖广告. CSS居中
- redis数据结构整理(二)
摘要: 1.各个数据结构的应用举例 1.1 String类型应用举例 1.2List类型应用举例 1.3Set类型应用举例 1.4Sorted Set类型应用举例 1.5Hash类型应用举例 内容: ...
- jQuery插件开发的五种形态[转]
这篇文章主要介绍了jQuery插件开发的五种形态小结,具体的内容就是解决javascript插件的8种特征,非常的详细. 关于jQuery插件的开发自己也做了少许研究,自己也写过多个插件,在自己的团队 ...
- piap.windows io 监测attilax总结
piap.windows io 监测attilax总结 当硬盘光狂闪的时候. 主要目标:找出哪个进程占用io最多, 作者Attilax 艾龙, EMAIL:1466519819@qq.com 来 ...
- Android 坐标系和 MotionEvent 分析、滑动
1.Android坐标系 在Android中,屏幕最左上角的顶点作为Android坐标系的原点,这个点向左是X轴正方向,这个点向下是Y轴正方向. 系统提供了getLocationOnScreen(in ...
- chrome远程调试真机上的app
chrome远程调试真机上的app 看来要上真机了...
- sql 循环处理表数据中当前行和上一行中某值相+/-
曾经,sql中循环处理当前行数据和上一行数据浪费了我不少时间,学会后才发现如此容易,其实学问就是如此,难者不会,会者不难. 以下事例,使用游标循环表#temptable中数据,然后让当前行和上一行中的 ...