线程池(ThreadPool)

线程池(ThreadPool)

https://www.cnblogs.com/jonins/p/9369927.html

线程池概述
由系统维护的容纳线程的容器,由CLR控制的所有AppDomain共享。线程池可用于执行任务、发送工作项、处理异步 I/O、代表其他线程等待以及处理计时器。

线程池与线程
性能:每开启一个新的线程都要消耗内存空间及资源(默认情况下大约1 MB的内存),同时多线程情况下操作系统必须调度可运行的线程并执行上下文切换,所以太多的线程还对性能不利。而线程池其目的是为了减少开启新线程消耗的资源(使用线程池中的空闲线程,不必再开启新线程,以及统一管理线程(线程池中的线程执行完毕后,回归到线程池内,等待新任务))。

时间:无论何时启动一个线程,都需要时间(几百毫秒),用于创建新的局部变量堆,线程池预先创建了一组可回收线程,因此可以缩短过载时间。

线程池缺点:线程池的性能损耗优于线程(通过共享和回收线程的方式实现),但是:

1.线程池不支持线程的取消、完成、失败通知等交互性操作。

2.线程池不支持线程执行的先后次序排序。

3.不能设置池化线程(线程池内的线程)的Name,会增加代码调试难度。

4.池化线程通常都是后台线程,优先级为ThreadPriority.Normal。

5.池化线程阻塞会影响性能(阻塞会使CLR错误地认为它占用了大量CPU。CLR能够检测或补偿(往池中注入更多线程),但是这可能使线程池受到后续超负荷的印象。Task解决了这个问题)。

6.线程池使用的是全局队列,全局队列中的线程依旧会存在竞争共享资源的情况,从而影响性能(Task解决了这个问题方案是使用本地队列)。

线程池工作原理
CLR初始化时,线程池中是没有线程的。在内部,线程池维护了一个操作请求队列。应用程序执行一个异步操作时,会将一个记录项追加到线程池的队列中。线程池的代码从这个队列中读取记录将这个记录项派发给一个线程池线程。如果线程池没有线程,就创建一个新线程。当线程池线程完成工作后,线程不会被销毁,相反线程会返回线程池,在那里进入空闲状态,等待响应另一个请求,由于线程不销毁自身,所以不再产生额外的性能损耗。

程序向线程池发送多条请求,线程池尝试只用这一个线程来服务所有请求,当请求速度超过线程池线程处理任务速度,就会创建额外线程,所以线程池不必创建大量线程。

如果停止向线程池发送任务,池中大量空闲线程将在一段时间后自己醒来终止自己以释放资源(CLR不同版本对这个事件定义不一)。

工作者线程&I/O线程
线程池允许线程在多个CPU内核上调度任务,使多个线程能并发工作,从而高效率的使用系统资源,提升程序的吞吐性。

CLR线程池分为工作者线程与I/O线程两种:

工作者线程(workerThreads):负责管理CLR内部对象的运作,提供”运算能力“,所以通常用于计算密集(compute-bound)性操作。

I/O线程(completionPortThreads):主要用于与外部系统交换信息(如读取一个文件)和分发IOCP中的回调。

注意:线程池会预先缓存一些工作者线程因为创建新线程的代价比较昂贵。

IO完成端口(IOCP)
IO完成端口(IOCP、I/O completion port):IOCP是一个异步I/O的API(可以看作一个消息队列),提供了处理多个异步I/O请求的线程模型,它可以高效地将I/O事件通知给应用程序。IOCP由CLR内部维护,当异步IO请求完成时,设备驱动就会生成一个I/O请求包(IRP、I/O Request Packet),并排队(先入先出)放入完成端口。之后会由I/O线程提取完成IRP并调用之前的委托。

I/O线程&IOCP&IRP:

当执行I/O操作时(同步I/O操作 and 异步I/O操作),都会调用Windows的API方法将当前的线程从用户态转变成内核态,同时生成并初始化一个I/O请求包,请求包中包含一个文件句柄,一个偏移量和一个Byte[]数组。I/O操作向内核传递请求包,根据这个请求包,windows内核确认这个I/O操作对应的是哪个硬件设备。这些I/O操作会进入设备自己的处理队列中,该队列由这个设备的驱动程序维护。

如果是同步I/O操作,那么在硬件设备操作I/O的时候,发出I/O请求的线程由于”等待“(无人任务处理)被Windows变成睡眠状态,当硬件设备完成操作后,再唤醒这个线程。所以性能不高,如果请求数很多,那么休眠的线程数也很多,浪费大量资源。

如果是异步I/O操作(在.Net中,异步的I/O操作都是以Beginxxx形式开始,内部实现为ThreadPool.BindHandle,需要传入一个委托,该委托会随着IRP一路传递到设备的驱动程序),该方法在Windows把I/O请求包发送到设备的处理队列后就会返回。同时,CLR会分配一个可用的线程用于继续执行接下来的任务,当任务完成后,通过IOCP提醒CLR它工作已经完成,当接收到通知后将该委托再放到CLR线程池队列中由I\O线程进行回调。

所以:大多数情况下,开发人员使用工作者线程,I/O线程由CLR调用(开发者并不会直接使用)。

基础线程池&工作者线程(ThreadPool)
.NET中使用线程池用到ThreadPool类,ThreadPool是一个静态类,定义于System.Threading命名空间,自.NET 1.1起引入。

调用方法QueueUserWorkItem可以将一个异步的计算限制操作放到线程池的队列中,这个方法向线程池的队列添加一个工作项以及可选的状态数据。
工作项:由callBack参数标识的一个方法,该方法由线程池线程调用。可向方法传递一个state实参(多于一个参数则需要封装为实体类)。

1 public static bool QueueUserWorkItem(WaitCallback callBack);
2 public static bool QueueUserWorkItem(WaitCallback callBack, object state);
下面是通过QueueUserWorkItem启动工作者线程的示例:

1 class Program
2 {
3 static void Main(string[] args)
4 {
5 //方式一
6 {
7 ThreadPool.QueueUserWorkItem(n => Test("Test-ok"));
8 }
9 //方式二
10 {
11 WaitCallback waitCallback = new WaitCallback(Test);
12 ThreadPool.QueueUserWorkItem(n => waitCallback("WaitCallback"));//两者效果相同 ThreadPool.QueueUserWorkItem(waitCallback,"Test-ok");
13 }
14 //方式三
15 {
16 ParameterizedThreadStart parameterizedThreadStart = new ParameterizedThreadStart(Test);
17 ThreadPool.QueueUserWorkItem(n => parameterizedThreadStart("ParameterizedThreadStart"));
18 }
19 //方式四
20 {
21 TimerCallback timerCallback = new TimerCallback(Test);
22 ThreadPool.QueueUserWorkItem(n => timerCallback("TimerCallback"));
23 }
24 //方式五
25 {
26 Action action = Test;
27 ThreadPool.QueueUserWorkItem(n => Test("Action"));
28 }
29 //方式六
30 ThreadPool.QueueUserWorkItem((o) =>
31 {
32 var msg = "lambda";
33 Console.WriteLine("执行方法:{0}", msg);
34 });
35
36 ......
37
38 Console.ReadKey();
39 }
40 static void Test(object o)
41 {
42 Console.WriteLine("执行方法:{0}", o);
43 }
44 /
45
作者:Jonins
46 * 出处:http://www.cnblogs.com/jonins/
47 */
48 }
执行结果如下:

以上是使用线程池的几种写法,WaitCallback本质上是一个参数为Object类型无返回值的委托

1 public delegate void WaitCallback(object state);
所以符合要求的类型都可以如上述示例代码作为参数进行传递。

线程池常用方法
ThreadPool常用的几个方法如下:

方法 说明
QueueUserWorkItem 启动线程池里的一个线程(工作者线程)
GetMinThreads 检索线程池在新请求预测中能够按需创建的线程的最小数量。
GetMaxThreads 最多可用线程数,所有大于此数目的请求将保持排队状态,直到线程池线程由空闲。
GetAvailableThreads 剩余空闲线程数。
SetMaxThreads 设置线程池中的最大线程数(请求数超过此值则进入队列)。
SetMinThreads 设置线程池最少需要保留的线程数。
示例代码:

1 static void Main(string[] args)
2 {
3 //声明变量 (工作者线程计数 Io完成端口计数)
4 int workerThreadsCount, completionPortThreadsCount;
5 {
6 ThreadPool.GetMinThreads(out workerThreadsCount, out completionPortThreadsCount);
7 Console.WriteLine("最小工作线程数:{0},最小IO线程数{1}", workerThreadsCount, completionPortThreadsCount);
8 }
9 {
10 ThreadPool.GetMaxThreads(out workerThreadsCount, out completionPortThreadsCount);
11 Console.WriteLine("最大工作线程数:{0},最大IO线程数{1}", workerThreadsCount, completionPortThreadsCount);
12 }
13 ThreadPool.QueueUserWorkItem((o) => {
14 Console.WriteLine("占用1个池化线程");
15 });
16 {
17 ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
18 Console.WriteLine("剩余工作线程数:{0},剩余IO线程数{1}", workerThreadsCount, completionPortThreadsCount);
19 }
20 Console.ReadKey();
21 }
执行的结果:

注意:

1.线程有内存开销,所以线程池内的线程过多而没有完全利用是对内存的一种浪费,所以需要对线程池限制最小线程数量。

2.线程池最大线程数是线程池最多可创建线程数,实际情况是线程池内的线程数是按需创建。

I/O线程
I\O线程是.NET专为访问外部资源所引入的一种线程,访问外部资源时为了防止主线程长期处于阻塞状态,.NET为多个I/O操作建立了异步方法。例如:

FileStream:BeginRead、BeginWrite。调用BeginRead/BeginWrite时会发起一个异步操作,但是只有在创建FileStream时传入FileOptions.Asynchronous参数才能获取真正的IOCP支持,否则BeginXXX方法将会使用默认定义在Stream基类上的实现。Stream基类中BeginXXX方法会使用委托的BeginInvoke方法来发起异步调用——这会使用一个额外的线程来执行任务(并不受IOCP支持,可能额外增加性能损耗)。

DNS:BeginGetHostByName、BeginResolve。

Socket:BeginAccept、BeginConnect、BeginReceive等等。

WebRequest:BeginGetRequestStream、BeginGetResponse。

SqlCommand:BeginExecuteReader、BeginExecuteNonQuery等等。这可能是开发一个Web应用时最常用的异步操作了。如果需要在执行数据库操作时得到IOCP支持,那么需要在连接字符串中标记Asynchronous Processing为true(默认为false),否则在调用BeginXXX操作时就会抛出异常。

WebServcie:例如.NET 2.0或WCF生成的Web Service Proxy中的BeginXXX方法、WCF中ClientBase的InvokeAsync方法。

这些异步方法的使用方式都比较类似,都是以Beginxxx开始(内部实现为ThreadPool.BindHandle),以Endxxx结束。

注意:

1.对于APM而言必须使用Endxxx结束异步,否则可能会造成资源泄露。

2.委托的BeginInvoke方法并不能获得IOCP支持。

3.IOCP不占用线程。

下面是使用WebRequest的一个示例调用异步API占用I/O线程:

1 class Program
2 {
3 static void Main(string[] args)
4 {
5 int workerThreadsCount, completionPortThreadsCount;
6 ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
7 Console.WriteLine("剩余工作线程数:{0},剩余IO线程数{1}", workerThreadsCount, completionPortThreadsCount);
8 //调用WebRequest类的异步API占用IO线程
9 {
10 WebRequest webRequest = HttpWebRequest.Create("http://www.cnblogs.com/jonins");
11 webRequest.BeginGetResponse(result =>
12 {
13 Thread.Sleep(2000);
14 Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ":执行最终响应的回调");
15 WebResponse webResponse = webRequest.EndGetResponse(result);
16 }, null);
17 }
18 Thread.Sleep(1000);
19 ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
20 Console.WriteLine("剩余工作线程数:{0},剩余IO线程数{1}", workerThreadsCount, completionPortThreadsCount);
21 Console.ReadKey();
22 }
23 }
执行结果如下:

有关I/O线程的内容点到此为止,感觉更多是I/O操作、文件等方面的知识点跟线程池瓜葛不多,想了解更多戳:这里

执行上下文
每个线程都关联了一个执行上下文数据结构,执行上下文(execution context)包括:

1.安全设置(压缩栈、Thread的Principal属性、winodws身份)。

2.宿主设置(System.Threading.HostExecutionContextManager)。

3.逻辑调用上下文数据(System.Runtime.Remoting.Messaging.CallContext的LogicalGetData和LogicalSetData方法)。

线程执行它的代码时,一些操作会受到线程执行上下文限制,尤其是安全设置的影响。

当主线程使用辅助线程执行任务时,前者的执行上下文“流向”(复制到)辅助线程,这确保了辅助线程执行的任何操作使用的是相同的安全设置和宿主设置。

默认情况下,CLR自动造成初始化线程的执行上下文“流向”任何辅助线程。但这会对性能造成影响。执行上下包含的大量信息采集并复制到辅助线程要耗费时间,如果辅助线程又采用了更多的辅助线程还必须创建和初始化更多的执行上下文数据结构。

System.Threading命名空间的ExecutionContext类,它允许控制线程执行上下文的流动:

1 class Program
2 {
3 static void Main(string[] args)
4 {
5 //将一些数据放到主函数线程的逻辑调用上下文中
6 CallContext.LogicalSetData("Action", "Jonins");
7 //初始化要由另一个线程做的一些事情,线程池线程能访问逻辑上下文数据
8 ThreadPool.QueueUserWorkItem(state => Console.WriteLine("辅助线程A:" + Thread.CurrentThread.ManagedThreadId + ";Action={0}", CallContext.LogicalGetData("Action")));
9 //现在阻止主线程执行上下文流动
10 ExecutionContext.SuppressFlow();
11 //初始化要由另一个线程做的一些事情,线程池线程能访问逻辑上下文数据
12 ThreadPool.QueueUserWorkItem(state => Console.WriteLine("辅助线程B:" + Thread.CurrentThread.ManagedThreadId + ";Action={0}", CallContext.LogicalGetData("Action")));
13 //恢复主线程的执行上下文流动,以避免使用更多的线程池线程
14 ExecutionContext.RestoreFlow();
15 Console.ReadKey();
16 }
17 }
结果如下:

ExecutionContext类阻止上下文流动以提升程序的性能,对于服务器应用程序,性能的提升可能非常显著。但是客户端应用程序的性能提升不了多少。另外,由于SuppressFlow方法用[SecurityCritical]特性标记,所以某些客户端如Silverlight中是无法调用的。

注意:

1.辅助线程在不需要或者不访问上下文信息时,应阻止执行上下文的流动。

2.执行上下文流动的相关知识,在使用Task对象以及发起异步I/O操作时,同样有用。

三种异步模式(扫盲)&BackgroundWorker
1.APM&EAP&TAP
.NET支持三种异步编程模式分别为APM、EAP和TAP:

1.基于事件的异步编程设计模式 (EAP,Event-based Asynchronous Pattern)

EAP的编程模式的代码命名有以下特点:

1.有一个或多个名为 “[XXX]Async” 的方法。这些方法可能会创建同步版本的镜像,这些同步版本会在当前线程上执行相同的操作。
2.该类还可能有一个 “[XXX]Completed” 事件,监听异步方法的结果。
3.它可能会有一个 “[XXX]AsyncCancel”(或只是 CancelAsync)方法,用于取消正在进行的异步操作。

2.异步编程模型(APM,Asynchronous Programming Model)

APM的编程模式的代码命名有以下特点:

1.使用 IAsyncResult 设计模式的异步操作是通过名为[BeginXXX] 和 [EndXXX] 的两个方法来实现的,这两个方法分别开始和结束异步操作 操作名称。例如,FileStream 类提供 BeginRead 和 EndRead 方法来从文件异步读取字节。

2.在调用 [BeginXXX] 后,应用程序可以继续在调用线程上执行指令,同时异步操作在另一个线程上执行。 每次调用 [BeginXXX] 时,应用程序还应调用 [EndXXX] 来获取操作的结果。

3.基于任务的编程模型(TAP,Task-based Asynchronous Pattern)

基于 System.Threading.Tasks 命名空间的 Task 和 Task,用于表示任意异步操作。 TAP之后再讨论。关于三种异步操作详细说明请戳:这里

2.BackgroundWorker
BackgroundWorker本质上是使用线程池内工作者线程,不过这个类已经多余了(了解即可)。在BackgroundWorker的DoWork属性追加自定义方法,通过RunWorkerAsync将自定义方法追加进池化线程内处理。

DoWork本质上是一个事件(event)。委托类型限制为无返回值且参数有两个分别为Object和DoWorkEventArgs类型。

1 public event DoWorkEventHandler DoWork;
2
3 public delegate void DoWorkEventHandler(object sender, DoWorkEventArgs e);
示例如下:

1 class Program
2 {
3 static void Main(string[] args)
4 {
5 int workerThreadsCount, completionPortThreadsCount;
6 ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
7 Console.WriteLine("剩余工作线程数:{0},剩余IO线程数{1}", workerThreadsCount, completionPortThreadsCount);
8 {
9 BackgroundWorker backgroundWorker = new BackgroundWorker();
10 backgroundWorker.DoWork += DoWork;
11 backgroundWorker.RunWorkerAsync();
12 }
13 Thread.Sleep(1000);
14 ThreadPool.GetAvailableThreads(out workerThreadsCount, out completionPortThreadsCount);
15 Console.WriteLine("剩余工作线程数:{0},剩余IO线程数{1}", workerThreadsCount, completionPortThreadsCount);
16 Console.ReadKey();
17 }
18 private static void DoWork(object sender, DoWorkEventArgs e)
19 {
20 Thread.Sleep(2000);
21 Console.WriteLine("demo-ok");
22 }
23 }
内部占用线程内线程,结果如下:

结语
程序员使用线程池更多的是使用线程池内的工作者线程进行逻辑编码。

相对于单独操作线程(Thread),线程池(ThreadPool)能够保证计算密集作业的临时过载不会引起CPU超负荷(激活的线程数量多于CPU内核数量,系统必须按时间片执行线程调度)。

超负荷会影响性能,因为划分时间片需要大量的上下文切换开销,并且使CPU缓存失效,而这些是处理器实现高效的必要调度。

CLR能够将任务进行排序,并且控制任务启动数量,从而避免线程池超负荷。CLR首先运行与硬件内核数量一样多的并发任务,然后通过爬山算法调整并发数量,保证程序切合最优性能曲线。

参考文献
CLR via C#(第4版) Jeffrey Richter

C#高级编程(第10版) C# 6 & .NET Core 1.0 Christian Nagel

果壳中的C# C#5.0权威指南 Joseph Albahari

http://www.cnblogs.com/dctit/

http://www.cnblogs.com/kissdodog/

http://www.cnblogs.com/JeffreyZhao/

...

原文地址:https://www.cnblogs.com/Leo_wl/p/9463985.html

时间: 2024-08-30 08:19:37

线程池(ThreadPool)的相关文章

C#多线程学习 之 线程池[ThreadPool](转)

在多线程的程序中,经常会出现两种情况: 一种情况:   应用程序中,线程把大部分的时间花费在等待状态,等待某个事件发生,然后才能给予响应                   这一般使用ThreadPool(线程池)来解决: 另一种情况:线程平时都处于休眠状态,只是周期性地被唤醒                   这一般使用Timer(定时器)来解决: 本篇文章单单讲线程池[ThreadPool] ThreadPool类 MSDN帮助信息: http://msdn.microsoft.com/z

43_2013年11月22日 线程池 Socket(Thread Lock Process 摇奖 线程池ThreadPool)

1>模拟线程池,生产者消费者问题 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Product { class Program { static void Main(string[] args) { //创建一个池子 MyConncetion[]

高效线程池(threadpool)的实现

高效线程池(threadpool)的实现 Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线程模型的变种,无论其怎么演化,其核心组件都包含了Reactor实例(提供事件注册.注销.通知功能).多路复用器(由操作系统提供,比如kqueue.select.epoll等).事件处理器(负责事件的处理)以及事件源(linux中这就是描述符)这四个组件.一般,会单独启动一个线程运行Reactor实例

多线程系列(2)线程池ThreadPool

上一篇文章我们总结了多线程最基础的知识点Thread,我们知道了如何开启一个新的异步线程去做一些事情.可是当我们要开启很多线程的时候,如果仍然使用Thread我们需要去管理每一个线程的启动,挂起和终止,显然是很麻烦的一件事情.还好.net framework为我们提供了线程池ThreadPool来帮助我们来管理这些线程,这样我们就不再需要手动地去终止这些线程.这一篇文章就让我们来学习一下线程池ThreadPool吧.关于它我想从以下几个方面进行总结. 认识线程池ThreadPool Thread

C#多线程学习 之 线程池[ThreadPool]

在多线程的程序中,经常会出现两种情况: 一种情况:   应用程序中,线程把大部分的时间花费在等待状态,等待某个事件发生,然后才能给予响应                   这一般使用ThreadPool(线程池)来解决: 另一种情况:线程平时都处于休眠状态,只是周期性地被唤醒                   这一般使用Timer(定时器)来解决: 本篇文章单单讲线程池[ThreadPool] ThreadPool类 MSDN帮助信息: http://msdn.microsoft.com/z

多线程二:线程池(ThreadPool)

在上一篇中我们讲解了多线程的一些基本概念,并举了一些例子,在本章中我们将会讲解线程池:ThreadPool. 在开始讲解ThreadPool之前,我们先用下面的例子来回顾一下以前讲过的Thread. 1 private void Threads_Click(object sender, EventArgs e) 2 { 3 Console.WriteLine($"****************btnThreads_Click Start {Thread.CurrentThread.Manage

多线程Thread,线程池ThreadPool

首先我们先增加一个公用方法DoSomethingLong(string name),这个方法下面的举例中都有可能用到 1 #region Private Method 2 /// <summary> 3 /// 一个比较耗时耗资源的私有方法 4 /// </summary> 5 /// <param name="name"></param> 6 private void DoSomethingLong(string name) 7 { 8

Nodejs事件引擎libuv源码剖析之:高效线程池(threadpool)的实现

声明:本文为原创博文,转载请注明出处. Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线程模型的变种,无论其怎么演化,其核心组件都包含了Reactor实例(提供事件注册.注销.通知功能).多路复用器(由操作系统提供,比如kqueue.select.epoll等).事件处理器(负责事件的处理)以及事件源(linux中这就是描述符)这四个组件.一般,会单独启动一个线程运行Reactor实例来

使用C++11封装线程池ThreadPool

读本文之前,请务必阅读: 使用C++11的function/bind组件封装Thread以及回调函数的使用 Linux组件封装(五)一个生产者消费者问题示例   线程池本质上是一个生产者消费者模型,所以请熟悉这篇文章:Linux组件封装(五)一个生产者消费者问题示例. 在ThreadPool中,物品为计算任务,消费者为pool内的线程,而生产者则是调用线程池的每个函数. 搞清了这一点,我们很容易就需要得出,ThreadPool需要一把互斥锁和两个同步变量,实现同步与互斥. 存储任务,当然需要一个

完全解析线程池ThreadPool原理&amp;使用

目录 1. 简介 2. 工作原理 2.1 核心参数 线程池中有6个核心参数,具体如下 上述6个参数的配置 决定了 线程池的功能,具体设置时机 = 创建 线程池类对象时 传入 ThreadPoolExecutor类 = 线程池的真正实现类 开发者可根据不同需求 配置核心参数,从而实现自定义线程池 // 创建线程池对象如下 // 通过 构造方法 配置核心参数 Executor executor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAXIMUM_POO