c#中的线程池(threadpool)
一个.NET进程中的CLR在进程初始化时,CLR会开辟一块内存空间给ThreadPool,默认ThreadPool默认没有线程,在内部会维护一个任务请求队列,当这个队列存在任务时,线程池则会通过开辟工作线程(都是后台线程)去请求该队列执行任务,任务执行完毕则回返回线程池,线程池尽可能会用返回的工作线程去执行(减少开辟),如果没返回线程池,则会开辟新的线程去执行,而后执行完毕又返回线程池,大概线程池模型如下:
线程池中的线程是由工作者线程和I/O线程组成的,CLR允许开发人员设置线程池需要创建的最大线程数量,但需要注意的是,我们一般不为线程池中的线程数设置上限,因为将会导致死锁,假设请求队列中有1000个工作项,但这些工作项全都因为一个事件而阻塞,等第1001个工作项发出信号才能接除阻塞,如果设置了最大1000个线程,第1001个工作项就不会执行,所有1000个线程都会一致阻塞,最终用户将被终止应用程序,并丢失他们的所有未保存的工作。
//获取默认线程池允许开辟的最大工作线程数和最大I/O异步线程树 ThreadPool.GetMaxThreads(out int maxWorkThreadCount, out int maxIOThreadCount); Console.WriteLine($"maxWorkThreadCount:{maxWorkThreadCount},maxIOThreadCount:{maxIOThreadCount}"); //获取默认线程池并发工作线程数和I/O异步线程数 ThreadPool.GetMinThreads(out int minWorkThreadCount, out int minIOThreadCount); Console.WriteLine($"minWorkThreadCount:{minWorkThreadCount},minIOThreadCount:{minIOThreadCount}"); for (int i = 0; i < 20; i++) { ThreadPool.QueueUserWorkItem(s => { var workThreadId = Thread.CurrentThread.ManagedThreadId; var isBackground = Thread.CurrentThread.IsBackground; var isThreadPool = Thread.CurrentThread.IsThreadPoolThread; Console.WriteLine($"work is on thread {workThreadId}, Now time:{DateTime.Now.ToString("ss.ff")}," + $" isBackground:{isBackground}, isThreadPool:{isThreadPool}"); Thread.Sleep(5000);//模拟工作线程运行 }); } Console.ReadLine();
输出结果如下:
可以看出:
由于我的CPU为4线程,默认线程池给我分配了4条工作线程和I/O线程,保证在该进程下实现真正的并行,可以看到前4条工作线程的启动时间是一致的,四条线程之后的五条线程,线程池尝试去用之前的工作线程去请求那个任务队列执行任务,由于前4条还在运行没返回到线程池,则每相隔一秒,创建新的工作线程去请求执行,而且该开辟的最多线程数是和线程池允许开辟的最大工作线程树和最大I/O异步线程数有关的,前9条执行完之后,最开始的4条线程已经返回到线程池当中,以此类推。
我们可以通过ThreadPool.SetMaxThreads 将工作线程数设置最多只有16,在执行任务前新增几行代码:
static void Main(string[] args) { //获取默认线程池允许开辟的最大工作线程数和最大I/O异步线程树 ThreadPool.GetMaxThreads(out int maxWorkThreadCount, out int maxIOThreadCount); Console.WriteLine($"maxWorkThreadCount:{maxWorkThreadCount},maxIOThreadCount:{maxIOThreadCount}"); //获取默认线程池并发工作线程数和I/O异步线程数 ThreadPool.GetMinThreads(out int minWorkThreadCount, out int minIOThreadCount); Console.WriteLine($"minWorkThreadCount:{minWorkThreadCount},minIOThreadCount:{minIOThreadCount}"); //新增加的代码 var success = ThreadPool.SetMaxThreads(4, 4);//只能设置>=最小并发工作线程数和I/O线程数 Console.WriteLine($"SetMaxThreads success:{success}"); ThreadPool.GetMaxThreads(out int maxWorkThreadCountNew, out int maxIOThreadCountNew); Console.WriteLine($"maxWorkThreadCountNew:{maxWorkThreadCountNew},maxIOThreadCountNew:{ maxIOThreadCountNew}"); for (int i = 0; i < 20; i++) { ThreadPool.QueueUserWorkItem(s => { var workThreadId = Thread.CurrentThread.ManagedThreadId; var isBackground = Thread.CurrentThread.IsBackground; var isThreadPool = Thread.CurrentThread.IsThreadPoolThread; Console.WriteLine($"work is on thread {workThreadId}, Now time:{DateTime.Now.ToString("ss.ff")}," + $" isBackground:{isBackground}, isThreadPool:{isThreadPool}"); Thread.Sleep(5000);//模拟工作线程运行 }); } Console.ReadLine(); }
输出结果如下:
可以很清楚知道,由于线程池最多只允许开辟4条工作线程和I/O线程,那么在线程池再开辟了4条线程之后,将不会再开辟新线程,新的任务也只能等前面的工作线程执行完回线程池后,再用返回的线程去执行新任务,导致新任务的开始执行时间会在5秒后
ThreadPool的优点如下:
- 默认线程池已经根据自身CPU情况做了配置,在需要复杂多任务并行时,智能在时间和空间上做到均衡,在CPU密集型操作有一定优势,而不是像Thread.Start那样,需要自己去判断和考虑
- 同样可以通过线程池一些方法,例如ThreadPool.SetMaxThreads手动配置线程池情况,很方便去模拟不同电脑硬件的执行情况
- 有专门的I/O线程,能够实现非阻塞的I/O,I/O密集型操作有优势(后续Task会提到)
但同样,缺点也很明显:
- ThreadPool原生不支持对工作线程取消、完成、失败通知等交互性操作,同样不支持获取函数返回值,灵活度不够,Thread原生有Abort (同样不推荐)、Join等可选择
- 不适合LongTask,因为这类会造成线程池多创建线程(上述代码可知道),这时候可以单独去用Thread去执行LongTask