【C# 线程】线程池 ThreadPool
阅读原文时间:2023年07月10日阅读:1

如今的应用程序越来越复杂,我们常常需要使用《异步编程:线程概述及使用》中提到的多线程技术来提高应用程序的响应速度。这时我们频繁的创建和销毁线程来让应用程序快速响应操作,这频繁的创建和销毁无疑会降低应用程序性能,我们可以引入缓存机制解决这个问题,此缓存机制需要解决如:缓存的大小问题、排队执行任务、调度空闲线程、按需创建新线程及销毁多余空闲线程……如今微软已经为我们提供了现成的缓存机制:线程池

1、.NET框架为每一个进程提供了一个线程池,每当您启动线程时,都会花费几百微秒来组织诸如新的私有局部变量堆栈之类的东西。
2、只有全局一个队列和n本地线程任务队列,无法取消任务,无法限制任务执行速度等等
3、当一个等待操作完成时,线程池中的一个辅助线程就会执行对应的回调函数
4、线程池中的线程由系统进行管理,程序员不需要费力于线程管理,可以集中精力处理应用程序任务。
5、线程池线程都是后台线程。每个线程都使用默认堆栈大小1MB,以默认的优先级运行,并处于多线程单元中,您可以随意更改池线程的优先级— 当释放回池时,它将恢复正常。线程池通过共享和回收线程来减少这些开销,从而允许在非常精细的级别应用多线程,而不会降低性能。
6、如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间之后创建另一个辅助线程。
7、但线程的数目永远不会超过最大值。超过最大值的其他线程可以排队,但它们要等到其他线程完成后才启动。
8、如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。
9、一种是用于处理CPU密集型逻辑的工作线程,即它主要使用CPU来执行其逻辑,如任何计算等,另一种是I / O线程,这些线程用于执行I / O请求或耗时的请求,例如读/写到文件系统, 调用数据库或调用任何第三方 API/请求。
10、线程池不保证全局队列工作项的处理顺序
11、如果工作项完成的时间太长(具体多长没有正式公布)线程池会创建更多的工作者线程,如果工作像完成速度开始变快,工作者线程会被销毁
线程池从其池中的一个线程开始。分配任务时,池管理器会"注入"新线程以处理额外的并发工作负载,最高可达最大限制。在足够长的不活动时间后,如果池管理器怀疑这样做会带来更好的吞吐量,则池管理器可能会"停用"线程。
您可以通过调用 来设置池将创建的线程的上限;默认值为:ThreadPool.SetMaxThreads
12、如果全局队列也为空,工作者线程会进入睡眠状态等待事情的发生,如果睡眠了太长时间,他会自己醒来并销毁至深允许系统回收线程使用的资源
13、最好是将线程池看成一个黑盒,不要拿单个应用程序去衡量它的性能,因为它不是针对某个单独应用程序而设计的。线程池内部会更改它的管理线程的方式,所以大多应用程序的性能会变的越来越好

线程池的全局队列(global Queue)

当调用ThreadPool.QueueUserWorkItem()添加工作项时,该工作项会被添加到线程池的全局队列中。线程池中的空闲线程以FIFO的顺序将工作项从全局队列中取出并执行,但并不能保证按某个指定的顺序完成。

线程的全局队列是共享资源,所以内部会实现一个锁机制。当一个任务内部会创建很多子任务时,并且这些子任务完成得非常快,就会造成频繁的进入全局队列和移出全局队列,从而降低应用程序的性能。基于此原因,线程池引擎为每个线程引入了局部队列。

线程的局部队列(local Queue)

为我们带来两个性能优势:任务内联化(task inlining)和工作窃取机制。

1)   任务内联化(task inlining)----活用顶层任务工作线程

static void Main(string[] args)
{
Task headTask= new Task(() =>
{
DoSomeWork(null);
});
headTask.Start();
Console.Read();
}
private static void DoSomeWork(object obj)
{
Console.WriteLine("任务headTask运行在线程“{0}”上",
Thread.CurrentThread.ManagedThreadId);

var taskTop = new Task(() =>  
{  
    Thread.Sleep(500);  
    Console.WriteLine("任务taskTop运行在线程“{0}”上",  
        Thread.CurrentThread.ManagedThreadId);  
});  
var taskCenter = new Task(() =>  
{  
    Thread.Sleep(500);  
    Console.WriteLine("任务taskCenter运行在线程“{0}”上",  
        Thread.CurrentThread.ManagedThreadId);  
});  
var taskBottom = new Task(() =>  
{  
    Thread.Sleep(500);  
    Console.WriteLine("任务taskBottom运行在线程“{0}”上",  
        Thread.CurrentThread.ManagedThreadId);  
});  
taskTop.Start();  
taskCenter.Start();  
taskBottom.Start();  
Task.WaitAll(new Task\[\] { taskTop, taskCenter, taskBottom });  

}

分析:(目前内联机制只有出现在等待任务场景)

这个示例,我们从Main方法主线程中创建了一个headTask顶层任务并开启。在headTask任务中又创建了三个嵌套任务并最后WaitAll() 这三个嵌套任务执行完成(嵌套任务安排在局部队列)。此时出现的情况就是headTask任务的线程被阻塞,而“任务内联化”技术会使用阻塞的headTask的线程去执行局部队列中的任务。因为减少了对额外线程需求,从而提升了程序性能。

局部队列“通常”以LIFO的顺序抽取任务并执行,而不是像全局队列那样使用FIFO顺序。LIFO顺序通常用有利于数据局部性,能够在牺牲一些公平性的情况下提升性能。

数据局部性的意思是:运行最后一个到达的任务所需的数据都还在任何一个级别的CPU高速缓存中可用。由于数据在高速缓存中任然是“热的”,因此立即执行最后一个任务可能会获得性能提升。

2)  工作窃取机制----活用空闲工作线程

当一个工作线程的局部队列中有很多工作项正在等待时,而存在一些线程却保持空闲,这样会导致CPU资源的浪费。此时任务调度器(TaskScheduler)会让空闲的工作线程进入忙碌线程的局部队列中窃取一个等待的任务,并且执行这个任务。

由于局部队列为我们带来了性能提升,所以,我们应尽可能地使用TPL提供的服务(任务调度器(TaskScheduler)),而不是直接使用ThreadPool的方法。

我们用一个示例来说明:

1、.net 会为每个进程生成一个线程池。线程池的初始值线程数是 cpu逻辑内核数。后面连续建线程要每间隔500ms新建一个线程。即使突然并发大量任务也是按这个进度进来线程。
2、可以通过设置min thread(默认等于cpu内核数) 提高 线程池一开始的线程数,从而提高并发数。 后面连续建线程要每间隔500ms新建一个线程。
3、本地队列:线程池中的每一个线程都会绑定一个 ThreadPoolWorkQueueThreadLocals 实例,在 workStealingQueue 这个字段上保存着本地队列。
4、全局队列:每个进程只有一个全局队列, 是由 ThreadPoolWorkQueue 维护的,同时它也是整个队列系统的入口,直接被 ThreadPool 所引用。
5、在线程池线程A中生成的任务,会安排入当前线程A的任务队列。
6、当前线程池的线程A的任务队列已经执行完成,如果全局任务队列也没有任务了,就会去查看其他线程B的任务队列,如果其他线程B的任务队列还有很对没完成的,当前线程A就会偷它B的任务执行。
7、线程池使用IO模型有两种:IOCPI/O模型verlapped I/O模型

8、线程池将自己的线程划分工作者线程(辅助线程)和I/O线程。前者用于执行普通的操作,后者专用于异步IO,比如文件和网络请求,注意,分类并不说明两种线程本身有差别,内部依然是一样的。

参考:https://www.cnblogs.com/eventhorizon/p/15316955.html#ithreadpoolworkitem-%E5%AE%9E%E7%8E%B0%E7%B1%BB%E7%9A%84%E5%AE%9E%E4%BE%8B

在以下几种情况下,适合于使用线程池线程:

(1)不需要前台执行的线程。
(2)不需要在使用线程具有特定的优先级。
(3)线程的执行时间不易过长,否则会使线程阻塞。由于线程池具有最大线程数限制,因此大量阻塞的线程池线程可能会阻止任务启动。
(4)不需要将线程放入单线程单元。所有 ThreadPool 线程均不处于多线程单元中。
(5)不需要具有与线程关联的稳定标识,或使某一线程专用于某一任务。
(6)一种是在应用程序中,线程把大部分的时间花费在等待状态,等待某个事件发生,然后才能给予响应,这一般使用ThreadPool(线程池)来解决
(7)一种情况是在线程平时都处于休眠状态,只是周期性地被唤醒,这一般使用Timer(定时器)来解决。下面对ThreadPool类进行详细说明。

  • 通过任务并行库(来自框架 4.0)

  • 通过呼叫[ThreadPool.QueueUserWorkItem](http://www.albahari.com/threading/#_QueueUserWorkItem)

  • 通过异步委托

  • 通过[BackgroundWorker](http://www.albahari.com/threading/part3.aspx#_BackgroundWorker)

  • WCF、远程处理、ASP.NET 和 ASMX Web 服务应用程序服务器

  • [System.Timers.Timer](http://www.albahari.com/threading/part3.aspx#_Multithreaded_Timers)[System.Threading.Timer](http://www.albahari.com/threading/part3.aspx#_Multithreaded_Timers)

  • 异步结尾的框架方法,例如 on(基于事件的异步模式)和大多数XXX方法(异步编程模型模式)WebClient``Begin

  • 普林克

线程池从其池中的一个线程开始。分配任务时,池管理器会"注入"新线程以处理额外的并发工作负载,最高可达最大限制。在足够长的不活动时间后,如果池管理器怀疑这样做会带来更好的吞吐量,则池管理器可能会"停用"线程。
可以通过调用 来设置池将创建的线程的上限;默认值为:workerThreads:  默认大小逻辑cpucore数, completionPortThreads:  默认大小逻辑。
 max threads 初始值:32位平台 1023,64位平台 short.MaxValue32767。

32位环境中的  workerThreads:1023  默认大小逻辑cpucore数, completionPortThreads:25   默认大小逻辑cpucore数
64位环境中的  workerThreads:32767 默认大小逻辑cpucore数, completionPortThreads:1000  默认大小逻辑cpucore数
默认数字是由进程的关联掩(affinity mask)码决定的。

min threads 初始值:运行环境 CPU 核心数,可通过 ThreadPool.SetMinThreads 进行设置,参数有效范围是 [1, max threads]。(这些数字可能因硬件和操作系统而异。之所以存在许多线程,是为了确保某些线程被阻止时取得进展(在等待某些条件时空转,例如来自远程计算机的响应)。
您还可以通过调用 来设置下限。下限的作用更微妙:它是一种高级优化技术,指示池管理器在达到下限之前不要延迟线程的分配。当存在阻塞的线程时,提高最小线程计数可提高并发性(请参阅侧边栏)。ThreadPool.SetMinThreads

CompletionPort的知识点:https://www.cnblogs.com/cdaniu/p/15782960.html

1、通过调用 ThreadPool.QueueUserWorkItem 并传递 WaitCallback 委托来使用线程池。
2、通过使用 ThreadPool.RegisterWaitForSingleObject 并传递 WaitHandle(在向其发出信号或超时时,它将引发对由 WaitOrTimerCallback 委托包装的方法的调用)来将与等待操作相关的工作项排队到线程池中。
若要取消等待操作(即不再执行WaitOrTimerCallback委托),可调用RegisterWaitForSingleObject()方法返回的RegisteredWaitHandle的 Unregister 方法。
3、如果您知道调用方的堆栈与在排队任务执行期间执行的所有安全检查不相关,则还可以使用不安全的方法 ThreadPool.UnsafeQueueUserWorkItem 和 ThreadPool.UnsafeRegisterWaitForSingleObject。UnsafeQueueUserWorkItem 和 RegisterWaitForSingleObject都会捕获调用方的堆栈,此堆栈将在线程池线程开始执行任务时合并到线程池线程的堆栈中。如果需要进行安全检查,则必须检查整个堆栈,但它还具有一定的性能开销。使用“不安全的”方法调用并不会提供绝对的安全,但它会提供更好的性能。

由于饥饿和死锁的问题存在,所以不建议使用GetMaxThreads、SetMaxThreads、GetMinThreads、SetMinThreads、GetAvailableThreads--CLR Via C#

ThreadPool.QueueUserWorkItem 用法

int WorkThreadCount, CompletionThreadcCont;

ThreadPool.GetMaxThreads(out WorkThreadCount, out CompletionThreadcCont);

ThreadPool.SetMinThreads(1001, 10);
ThreadPool.GetMinThreads(out WorkThreadCount, out CompletionThreadcCont);// 默认最小值是cpu个数
ThreadPool.QueueUserWorkItem(new WaitCallback(count), null);

Console.WriteLine(ThreadPool.PendingWorkItemCount);//当前排队等候处理的工作项的数目。
Console.WriteLine(ThreadPool.CompletedWorkItemCount);//到目前为止已处理的工作项的数目。
Console.WriteLine(ThreadPool.ThreadCount);//获取当前程序的 线程池的线程数
ThreadPool.GetAvailableThreads(out WorkThreadCount, out CompletionThreadcCont);//获取剩余的可用线程
count(null);

void count(object? obg)
{

Console.WriteLine($"WorkThreadCount:{WorkThreadCount},CompeTionThreadcCont:{CompletionThreadcCont}");

}
//将方法委托给线程池
ThreadPool.QueueUserWorkItem( o => {
int i;

i = Convert.ToInt32(o);  
i++;  
Console.WriteLine("sfdsdf");  

},1 );

RegisteredWaitHandle RegisterWaitForSingleObject(WaitHandle, WaitOrTimerCallback, Object, TimeSpan, Boolean) 方法是定时器,定时执行某个函数
WaitHandle waitObject: 等待完成句柄 AutoSetWaitHandle、 ManualSetWaitHandle
WaitOrTimerCallback:回调函数
Object:委托参数
TimeSpan timeout:并指定一个 TimeSpan 值来表示超时时间。时间到了执行WaitOrTimerCallback函数。
Boolean executeOnlyOnce:是否只执行一次委托。如果为 true,表示在调用了委托后,线程将不再在 waitObject 参数上等待;如果为 false,表示每次完成等待操作后都重置计时器,直到等到信号(set())。
返回值:RegisteredWaitHandle,用来注销等待句柄

案例

Random random = new Random();
AutoResetEvent wh = new AutoResetEvent(false);//将初始状态设置为非终止
RegisteredWaitHandle RWH =ThreadPool.RegisterWaitForSingleObject(wh, writeangen, null, 2000, false);//2s true=执行一次结束
void writeangen(object? state, bool timedOut)
{
Console.WriteLine("Timeout ");//定时输出
}
Console.ReadLine();
wh.Set();
RWH.Unregister(wh);//注销等待句柄,应为

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章