C#.Net使用线程池(ThreadPool)与专用线程(Thread)


C#.Net使用线程池(ThreadPool)与专用线程(Thread)
线程池(ThreadPool)使用起来很简单,但它有一些限制:

1. 线程池中所有线程都是后台线程,如果进程的所有前台线程都结束了,所有的后台线程就会停止。不能把入池的线程改为前台线 程。

2. 不能给入池的线程设置优先级或名称。

3. 对于COM对象,入池的所有线程都是多线程单元(Multi-threaded apartment,MTA)线程。许多COM对象都需要单线程单元(Single -threaded apartment,STA)线程。

4.入池的线程只能用于时间较短的任务。如果线程要一直运行(如Word的拼写检查器线程),就应使用Thread类创建一个线程。




高效线程使用圣典


  严格来讲,线程的系统开销很大。系统必须为线程分配并初始化一个线程内核对象,还必须为每个线程保留1mb的地址空间 (按需提交)用于线程的用户模式堆栈,分配12kb左右的地址空间用于线程的内核模式堆栈。然后,紧接着线程创建后,windows调 用进程中每个dll都有的一个函数来通知进程中所有的dll操作系统创建了一个新的线程。同样,销毁一个线程的开销也不小:进程 中的每个dll都要接收一个关于线程即将“死亡”的通知,而且内核对象及堆栈还需释放。


  如果一台计算机中只有一个cpu,那么在某一时刻只有一个线程可以运行。windows必须跟踪记录线程对象,而且是不停地跟 踪记录每个线程对象。windows不得不决定cpu下次调度哪个线程来执行。这个额外的代码不得不每隔20ms左右执行一次。windows使 cpu停止执行一个线程的代码,而开始执行另一个线程的代码的现象,我们称之为上下文切换(context switch)。上下文切换的开 销相当大,因为操作系统必须执行以下步骤:


  1. 进入内核模式。


  2. 将cpu的寄存器保存到当前正在执行的线程的内核对象中。x86架构的机器上cpu寄存器占了大约700字节的空间;x64架构 的机器上cpu寄存器占了大约1240字节的空间;而在ia64架构的机器上cpu寄存器占了大约2500字节的空间。


  3. 需要一个自旋锁(spin lock),确定下一次调度哪个线程,然后再释放该自旋锁。如果下一次调度的线程属于另一个进 程,那么此处的开销会更大,因为操作系统必切换到虚拟地址空间。


  4. 将即将运行的线程的内核对象的值加载到cpu寄存器中。


  5. 退出内核模式。


  所有上述内容都是纯粹的开销,导致windows操作系统和应用程序的执行速度比在单线程系统上的执行速度慢。


  综合上述所有结果可得出以下结论:应尽可能地限制线程的使用。如果创建的线程越多,给操作系统带来的开销就越大,所 有的东西也就运行得越慢。另外,每个线程都需要资源(内核对象占用的内存及两个堆栈),所以每个线程都会消耗内存。


  线程还有另一个用途:可扩展性。当计算机有多个cpu时,windows能同时调度多个线程:每个cpu运行一个线程。


CLR线程池简介


  如前所述,创建并销毁一个线程在时间上的开销相当大。另外,线程多还会浪费内存资源,而且由于操作系统不得不在可运 行线程间进行调度和上下文切换,从而影响操作系统和应用程序的性能。为改进这种现象,clr中包含管理clr线程池的代码。我们 可以将线程池看作应用程序自己使用的线程的集合。每个进程都有一个线程池,这个线程池被该进程中的所有应用程序域共享。


  当clr初始化时,线程池中还没有任何线程。从内部实现上讲,线程池维护了一系列操作请求。应用程序希望执行一个异步 操作时,可以调用一些方法在线程池的队列中加入一个条目。线程池中的代码将从这个队列中提取出条目,并将该条目分派到线程 池中的线程。如果线程池中没有任何线程,就创建一个新的线程。创建一个线程会有相关的性能损失。但是,当线程池中的线程完 成任务时,并不会被销毁,而是返回到线程池中,在线程池中空闲,等待响应另外的请求。因为线程不对它自身进行销毁,所以此 处不会带来性能损失。


  如果应用程序对线程池进行了很多的请求,那么线程池将试图只用一个线程来响应所有的请求。但是,如果应用程序排队的 请求超出了线程池的处理能力,线程池中将创建另外的线程。最终,应用程序排队的请求与线程池中线程的处理能力达到一个平衡 点,我们可以采用较小数量的线程来处理所有的请求,因此线程池中也就不再需要创建更多的线程。


  如果应用程序停止请求线程池,线程池中可能会有许多不做事情的线程。这种情况会浪费内存资源。因此,当线程池中的线 程空闲超过大约2分钟后,线程将唤醒自己,并终止自己,以释放内存资源。当线程终止自己时,也会存在一个性能损失。但是,该 性能损失不是很严重,因为线程在终止自己时,线程已处于空闲状态,这意味着我们的应用程序当前没有执行太多的工作。


  从内部实现上讲,线程池将线程池中的线程进行分类,划分为工作线程(worker thread)和i/o线程(i/o thread)。当应 用程序请求线程池执行一个受计算限制的异步操作(包括初始化受i/o限制的异步操作)时使用工作线程,而i/o线程用于在受i/o限 制的异步操作完成时通知代码。具体而言,这意味着我们需要使用异步编程模型来进行i/o请求。


限制线程池中的线程数量


  clr的线程池允许开发人员设置工作线程和i/o线程的最大数量。clr保证创建的线程数量不会超过这个设置值。但永远不要 对线程池中线程的数量设置一个上限,因为饥饿和死锁现象可能会发生。在clr的2.0版默认中,工作线程的默认最大数量为机器中 每个cpu25个,i/o线程最大数量设为1000个。


  system.threading.threadpool类提供了几个操作线程池中线程数量的静态方法:getmaxthreads(查询线程池对线程数量的 最大限制)、setmax-threads(设置线程数量最大限制)、getminthreads(查询线程池对线程数量的最小限制)、setminthreads (设置线程数量最小限制)、getavailable-threads。


  强烈建议不要调用setmaxthreads方法修改线程池中线程数量的限制,因为这会导致损害应用程序的执行性能。


  clr的线程池试图避免过快地创建额外的线程。具体而言,线程池试图避免每隔500ms就创建一个新的线程。这对某些开发人 员而言,引发了一个问题,因为队列中的任务无法得到及时地处理。要处理此问题,可以调用setminthreads方法设置线程池中拥有 线程的最低数量。调用该方法后,线程池将很快地创建这么多的线程,并且当队列的任务继续增加,所创建的所有线程都被使用后 ,线程池还会按照每隔500ms的时间继续创建额外的线程。默认情况下,线程池中工作线程和i/o线程的最小数量被设为2,这个值可 以通过调用getminthreads方法获得。


  最后,可以通过调用getavailablethreads方法来获得线程池中可以增加的额外线程的数量。该方法的返回值为线程池中可 以拥有的线程的最大数量减去线程池中当前所拥有的线程数量。这个值仅在返回的那一刻有用,因为在方法返回后,线程池中可能 已经增加了许多线程,或有些线程可能已被销毁。


使用线程池执行受计算限制的异步操作


  受计算限制的操作是需要进行计算的操作。如,电子表格应用程序中可计算的单元。理想情况下,受计算限制的操作不会执 行任何异步i/o操作,因为所有的异步i/o操作在底层硬件执行工作时都将挂起调用线程。应该尽量使线程运行,因为挂起的线程不 再继续运行但仍然使用系统的资源。


  为了将一个受计算限制的异步操作加入到线程池的队列中,一般可以使用threadpool类中定义的下述方法:


static bool QueueUserWorkItem(WaitCallback callback);
static bool QueueUserWorkItem(WaitCallback callback, object state);
static bool UnsafeQueueUserWorkItem(WaitCallback callback, object state);


  上述方法将一个“工作项”(及可选的状态数据)加入到线程池的队列中,然后这些方法就会立即返回。工作项仅仅是一个 由callback参数标识的方法,线程池中的线程将调用该方法。该方法可以只传递一个单独的由state(状态数据)参数指定的参数。 没有state参数的QueueUserWorkItem方法为回调函数传递null。最终,线程池中的一些线程将执行工作项,从而导致我们的方法被 调用。我们写的回调方法必须匹配system.threading.WaitCallback委托类型,它的定义方式如下所示:

delegate void WaitCallback(object state);


  下面的代码演示了线程池中的线程如何异步调用一个方法:
 

using system;
using system.threading;

public static class program
{
public static void main()
{
console.writeline(
"main thread: queuing an asynchronous operation"); threadpool.QueueUserWorkItem(computeboundop, 5);
console.writeline(
"main thread: doing other work here ...");
thread.sleep(
10000); //模拟其他工作10秒钟
console.writeline("hit <enter> to end this program ...");
console.readline(); }

//该方法的签名必须与WaitCallback委托类型匹配
private static void computeboundop(object state)
{
//该方法由线程池中的线程执行
console.writeline("in computeboundop: state={0}", state);
thread.sleep(
1000); //模拟其他工作1秒钟
//在该方法返回后,线程就回到线程池中,然后等待执行另一个任务
}
}


  如果回调方法抛出的异常是未处理异常,那么clr将终止进程。


  threadpool类有一个UnsafeQueueUserWorkItem方法。该方法与平时调用的QueueUserWorkItem方法非常相似。下面先简单介 绍一下这两个方法的区别:试图访问一个受限资源(如打开一个文件)时,clr将执行一个代码访问安全(code access security, cas)检查。也就是说,clr将检查调用线程的调用堆栈中的所有程序集是否都有访问资源的许可权限。如果有一些程序集没有所需 的许可权限,clr将抛出一个securityexception异常。假设正在执行代码的线程所在的程序集没有打开文件的许可权限,那么在线 程试图打开文件时,clr将抛出一个securityexception异常。


  为让线程继续运行,线程可以在线程池的队列加入一个工作项,让线程池中的线程来执行打开文件的代码。当然这必须在拥 有合适许可权限的程序集中进行。这种“工作区”智取安全权限的现象可以允许怀恶意的代码对受限资源进行严重破坏。为阻止这 种获得安全权限的方式,QueueUserWorkItem方法内部遍历调用线程的堆栈,并捕获所有被授予的安全权限。然后,当线程池中的线 程开始执行时,这些权限再与线程结合。因此,线程池中的线程以调用QueueUserWorkItem方法的线程相同的权限集来完成运行。


  遍历线程的堆栈并捕获所有的安全权限与性能紧密相关。如果希望改进受计算限制的异步操作的排队性能,可以调用 UnsafeQueueUserWorkItem方法。该方法只将工作项加入到线程池的队列中,而不遍历调用线程的堆栈。最后结果是这个方法比 QueueUserWorkItem方法执行得快,但它在应用程序中打开了一个潜在的安全漏洞。仅当可以确认线程池中的线程执行的代码不触及 受限资源时,或确信接触这部分资源不会出现问题时,我们才可以调用unsafequeueuserwork-item方法。同样,还需注意调用该方 法需要使securitypermission的controlpolicy标记和controlevidence标记开启,可阻止未信任的代码偶然或故意提升它的许可权 限。


使用专用线程执行受计算限制的异步操作


  强烈建议大家尽量多用线程池来执行受计算限制的异步操作。但在有些情况下,我们可能希望显式创建一个线程,专门用于 执行特定的受计算限制的异步操作。一般情况下,如果即将执行的代码需要线程处于一个特定的状态(与线程池中线程的普通状态 不同),那么就希望创建一个专用的线程。如:希望线程以一个特殊的优先级运行(所有线程池中的线程都以普通优先级运行,而 且我们不应该修改线程池中线程的优先级),就需要创建一个专用的线程。再如:希望让一个线程成为前台线程(所有线程中的线 程都是后台线程),也可以考虑创建并使用自己的线程,从而阻止应用程序的“死亡”,直到线程完成任务。如果受计算限制的任 务运行的时间特别长,也应该使用专用线程,这样,我们就不必让线程池的逻辑去费力判断是否还需创建额外的线程。最后,如果 我们希望启动一个线程,然后通过调用thread的abort方法中断该线程的话,应该使用一个专用线程。


  为创建一个专用线程,我们可构建一个system.threading.thread类的实例(以方法的名称作为构造器的参数)。下面是构 造器的原型:
 

public sealed class thread : criticalfinalizerobject, ...
{
public thread(parameterizedthreadstart start);
}


  参数start用来标识专用线程的方法即将执行,这个方法必须与委托parameterizedthreadstart的签名相匹配:
 

delegate void parameterizedthreadstart(object obj);


  可看出,parameterizedthreadstart委托的签名与WaitCallback委托的签名相同。这意味着使用一个线程池中的线程或使用 一个专用线程就可以调用相同的方法。


  构建一个thread对象并不创建一个操作系统线程。为实际创建一个操作系统线程,并让它开始执行回调方法,我们必须调用 thread的start方法。如下所示:
 

using system;
using system.threading;

public static class program
{
public static void main()
{
console.writeline(
"main thread: starting a dedicated thread " + " to do an asynchronous operation");
thread dedicatedthread
= new thread(computeboundop);
dedicatedthread.start(
5); console.writeline("main thread: doing other work here...");
thread.sleep(
10000); //模拟其他工作10秒 dedicatedthread.join(); //等待线程终止 console.writeline("hit <enter> to end this program...");
console.readline();
}






//该方法的签名必须与parameterizedthreadstart委托匹配
private static void computeboundop(object state)
{
//该方法由一个专用线程执行
console.writeline("in computeboundop: state = {0}", state);
thread.sleep(
1000); //模拟其他工作1秒
}
}


  注意,main方法调用了join方法,而join方法导致调用线程停止执行任何代码,直到由dedicatedthread标识的线程自己销 毁自己或被终止。使用threadpool的QueueUserWorkItem方法将异步操作排队时,clr没有提供内置的方法来判断操作是否完成。而 join方法却在我们使用专用线程时为我们提供了这种能力。但是,如果需要知道操作是在什么时候完成的,就不应该使用专用线程 来取代QueueUserWorkItem方法,而应该使用apm。



转载文章,C/S框架网责任编辑


本文来源:
版权声明:本文为开发框架文库发布内容,转载请附上原文出处连接
C/S框架网
上一篇:C#.Net前台线程与后台线程的区别
下一篇:使用net.exe和sc.exe实用程序启动停止Windows服务
评论列表

发表评论

评论内容
昵称:
关联文章

C#.Net使用线(ThreadPool)专用线(Thread)
C#.Net前台线后台线的区别
C#多线使用读写锁ReaderWriterLockSlim同步写入文件
C#异步编程(多线
C#多线处理多个队列的数据(交叉线访问及Invoke方法使用)
C#,Asp.Net线断点续传下载
C# 多线入门 - 开发实例
尝试释放正在使用的RCW,活动线或其他线上正在使用该RCW
C#.NET7 多线播放mp3/wav音频|winmm.dll winapi播放音乐文件
.NET5 HttpClient多线并发请求阻塞“发生一个或多个错误”解决方案
C#异步操作等待窗体,异步多线处理数据通用界面(frmThreadOperating)
C#多线异步处理数据通用界面窗体(frmThreadOperating)
C#.NET理解Task和async await原理
C#.NET 后端WebApi接口搭建教,WebApi接口开发实例
SQL Server连接基本原理
SQLServer连接字符串连接详解,Pooling=True/False区别
SQL连接完整测试报告
C#.NET之间的关系
C#.Net 静态构造器使用详解
超时时间已到,但是尚未从中获取连接。出现这种情况可能是因为所有连接均在使用,并且达到了最大大小

热门标签
.NET5 .NET6 .NET7 APP Auth-软件授权注册系统 Axios B/S B/S开发框架 Bug Bug记录 C#加密解密 C#源码 C/S CHATGPT CMS系统 CodeGenerator CSFramework.DB CSFramework.EF CSFrameworkV1学习版 CSFrameworkV2标准版 CSFrameworkV3高级版 CSFrameworkV4企业版 CSFrameworkV5旗舰版 CSFrameworkV6.0 DAL数据访问层 Database datalock DbFramework Demo教学 Demo下载 DevExpress教程 DOM EF框架 Element-UI EntityFramework ERP ES6 Excel FastReport GIT HR IDatabase IIS JavaScript LINQ MES MiniFramework MIS NavBarControl Node.JS NPM OMS ORM PaaS POS Promise API Redis SAP SEO SQL SQLConnector TMS系统 Token令牌 VS2022 VSCode VUE WCF WebApi WebApi NETCore WebApi框架 WEB开发框架 Windows服务 Winform 开发框架 Winform 开发平台 WinFramework Workflow工作流 Workflow流程引擎 版本区别 报表 踩坑日记 操作手册 代码生成器 迭代开发记录 基础资料窗体 架构设计 角色权限 开发sce 开发技巧 开发教程 开发框架 开发平台 开发指南 客户案例 快速搭站系统 快速开发平台 秘钥 密钥 权限设计 软件报价 软件测试报告 软件简介 软件开发框架 软件开发平台 软件开发文档 软件体系架构 软件下载 软著证书 三层架构 设计模式 生成代码 实用小技巧 收钱音箱 数据锁 数据同步 微信小程序 未解决问题 文档下载 喜鹊ERP 喜鹊软件 系统对接 详细设计说明书 行政区域数据库 需求分析 疑难杂症 蝇量级框架 蝇量框架 用户管理 用户开发手册 用户控件 在线支付 纸箱ERP 智能语音收款机 自定义窗体 自定义组件 自动升级程序