C# 多线程(三)线程池

        

       

目录

1.通过TPL使用线程池    

2.不使用TPL进入线程池的办法

异步委托

3.线程池优化技术

最小线程数的工作原理


         每当启动一个新线程时,系统都需要花费数百微秒来分配资源,例如创建独立的局部变量栈空间。默认情况下,每个线程还会占用约1MB内存。线程池通过共享和回收线程来消除这些开销,使得多线程技术可以应用于非常细粒度的场景而不会造成性能损失。这在利用多核处理器以"分而治之"方式并行执行计算密集型代码时尤为有用。

线程池还会限制同时运行的线程总数。过多的活动线程会给操作系统带来管理负担,并导致CPU缓存失效。一旦达到限制,新任务将进入队列,只有当前线程完成任务后才能启动。这使得高并发应用(如Web服务器)的实现成为可能。(异步方法模式是一种更高级的技术,它能更高效地利用线程池中的线程,我们会在后面文章讲解)。

进入线程池有多种方式:

  • Task Parallel Library (Framework 4.0)
  • ThreadPool.QueueUserWorkItem
  • asynchronous delegates
  • BackgroundWorker

以下组件会间接使用线程池:
• WCF、远程处理(Remoting)、ASP.NET和ASMX Web服务应用服务器
• System.Timers.Timer和System.Threading.Timer计时器
• 以Async结尾的框架方法(如WebClient基于事件的异步模式)
• 大多数BeginXXX方法(异步编程模型模式)
• PLINQ并行查询

任务并行库(TPL)和PLINQ功能强大且抽象层次高,即使不考虑线程池优势也值得用于多线程开发。使用线程池时需注意以下事项:

  1. 无法设置线程池线程的Name属性,这会增加调试难度(但在Visual Studio线程窗口中可附加描述信息)

  2. 线程池线程默认都是后台线程(通常不影响使用)

  3. 在应用初期阻塞线程池线程可能导致额外延迟,除非调用ThreadPool.SetMinThreads(参见"优化线程池"章节)

  4. 可临时修改线程池线程优先级,但释放回池后优先级会自动重置为普通级别

可通过Thread.CurrentThread.IsThreadPoolThread属性检测当前是否运行在线程池线程上。

1.通过TPL使用线程池    

        使用任务并行库(TPL)中的Task类可以轻松进入线程池。Task类是在.NET Framework 4.0中引入的:如果您熟悉旧的结构,可以将非泛型Task类视为ThreadPool.QueueUserWorkItem的替代品,而泛型Task<TResult>则是异步委托的替代品。新结构比旧结构更快、更方便、更灵活。

要使用非泛型Task类,只需调用Task.Factory.StartNew并传入目标方法的委托即可:

static void Main()    // The Task class is in System.Threading.Tasks
{Task.Factory.StartNew (Go);
}static void Go()
{Console.WriteLine ("Hello from the thread pool!");
}

Task.Factory.StartNew函数返回一个Task类型对象,你可以使用这个对象管理这个任务,比如你可以用Waith方法等待任务完成。

注意:当调用任务的Wait方法时,任何未处理的异常都会便捷地重新抛回到宿主线程(如果不调用Wait而是直接放弃任务,未处理的异常将像普通线程一样导致进程关闭)。

        泛型Task<TResult>类是非泛型Task的子类,它允许在任务执行完成后获取返回值。在下面的示例中,我们使用Task<TResult>下载网页:

static void Main()
{// Start the task executing:Task<string> task = Task.Factory.StartNew<string>( () => DownloadString ("http://www.linqpad.net") );// We can do other work here and it will execute in parallel:RunSomeOtherMethod();// When we need the task's return value, we query its Result property:// If it's still executing, the current thread will now block (wait)// until the task finishes:string result = task.Result;
}static string DownloadString (string uri)
{using (var wc = new System.Net.WebClient())return wc.DownloadString (uri);
}

当查询任务的Result属性时,任何未处理的异常都会自动重新抛出(封装在AggregateException中)。但如果不查询Result属性(也不调用Wait方法),未处理的异常将会导致进程崩溃。

任务并行库(TPL)功能远不止于此,它特别适合利用多核处理器优势。我会在后续文章专门讨论TPL的更多功能。

2.不使用TPL进入线程池的办法

        如果您的开发目标是.NET Framework 4.0之前的版本,则无法使用任务并行库(TPL)。此时必须改用以下传统方式进入线程池:ThreadPool.QueueUserWorkItem和异步委托。两者的主要区别在于:

  1. 异步委托允许从线程返回数据

  2. 异步委托还能将异常封送回调用方

QueueUserWorkItem使用方法:
只需调用该方法并传入要在池线程上执行的委托即可:

static void Main()
{ThreadPool.QueueUserWorkItem (Go);ThreadPool.QueueUserWorkItem (Go, 123);Console.ReadLine();
}static void Go (object data)   // data will be null with the first call.
{Console.WriteLine ("Hello from the thread pool! " + data);
}

运行结果:

Hello from the thread pool!
Hello from the thread pool! 123

目标方法Go必须接受单个object参数(以满足WaitCallback委托)。这提供了传递数据的便捷方式,类似于ParameterizedThreadStart。但与Task不同,QueueUserWorkItem不会返回对象来帮助后续执行管理。此外,您必须显式处理目标代码中的异常——未处理的异常将导致程序崩溃。

异步委托


        ThreadPool.QueueUserWorkItem未提供简单机制来获取线程执行完成后的返回值。异步委托调用(简称异步委托)解决了这个问题,允许任意数量的类型化参数双向传递。更重要的是,异步委托上的未处理异常会便捷地重新抛回到原始线程(更准确地说,是调用EndInvoke的线程),因此不需要显式处理。

以下是使用异步委托启动工作任务的步骤:

  1. 实例化指向要在并行中运行方法的委托(通常使用预定义的Func委托)

  2. 调用委托的BeginInvoke,保存返回的IAsyncResult值BeginInvoke会立即返回,此时可执行其他操作

  3. 需要结果时,在委托上调用EndInvoke并传入保存的IAsyncResult对象

下例使用异步委托调用与主线程并发执行一个返回字符串长度的简单方法:

static void Main()
{Func<string, int> method = Work;IAsyncResult cookie = method.BeginInvoke ("test", null, null);//// ... here's where we can do other work in parallel...//int result = method.EndInvoke (cookie);Console.WriteLine ("String length is: " + result);
}static int Work (string s) { return s.Length; }

EndInvoke 主要完成三个关键操作:

  1. 等待异步委托完成执行(若尚未完成)

  2. 接收返回值(以及所有ref/out参数)

  3. 将工作线程中未处理的异常抛回调用线程

技术细节说明:
• 即使异步委托调用的方法没有返回值,严格来说仍需调用EndInvoke
• 实际上这一要求存在争议——毕竟没有"EndInvoke执法者"来惩罚违规者!
• 但若选择不调用EndInvoke,则必须自行处理工作方法的异常,避免静默失败

高级用法:
调用BeginInvoke时还可指定回调委托——即接受IAsyncResult参数的完成回调方法。这种模式允许发起线程"忘记"异步委托,但需要在回调端做一些额外工作:

static void Main()
{Func<string, int> method = Work;method.BeginInvoke ("test", Done, method);// ...//
}static int Work (string s) { return s.Length; }static void Done (IAsyncResult cookie)
{var target = (Func<string, int>) cookie.AsyncState;int result = target.EndInvoke (cookie);Console.WriteLine ("String length is: " + result);
}

BeginInvoke 的最后一个参数是用户状态对象,该对象会填充 IAsyncResult 的 AsyncState 属性。这个参数可以传递任意您需要的数据;在本例中,我们用它向完成回调传递方法委托,以便我们能够对其调用 EndInvoke。

3.线程池优化技术

        线程池初始时仅包含一个线程。当任务被分配时,池管理器会"注入"新线程以应对额外的并发工作负载,直至达到最大限制。在持续空闲足够长时间后,如果池管理器判断减少线程能提升吞吐量,则可能"回收"多余线程。

您可以通过ThreadPool.SetMaxThreads设置线程池创建线程的上限,各版本默认值为:
        • Framework 4.0(32位环境):1023个线程
        • Framework 4.0(64位环境):32768个线程
        • Framework 3.5:每个核心250个线程
        • Framework 2.0:每个核心25个线程
(具体数值可能因硬件和操作系统而异)。设置较高数量是为了确保当部分线程阻塞时(如等待远程计算机响应),程序仍能继续执行。

        通过ThreadPool.SetMinThreads还可设置下限线程数。下限的作用更为精妙:这是一种高级优化技术,指示池管理器在达到下限前不得延迟线程分配。当存在阻塞线程时,提高最小线程数可增强并发性(参见边栏说明)。

        默认下限为每个处理器核心1个线程——这是实现CPU完全利用的最低要求。但在服务器环境(如IIS下的ASP.NET)中,下限通常高得多,可达50个甚至更多。

最小线程数的工作原理

        将线程池的最小线程数提升至x,并不会立即强制创建x个线程——线程仅在需要时才会创建。实际上,这个设置是指导线程池管理器在需要时可立即创建最多x个线程。那么问题来了:为什么线程池在需要线程时会有意延迟创建呢?

原因在于防止短暂爆发的短期活动导致线程全量分配,从而突然增加应用程序的内存占用。举例来说,假设一台四核计算机运行的客户端应用一次性提交40个任务:

  • 若每个任务执行10毫秒的计算

  • 假设工作均匀分配到四个核心

  • 整个过程将在100毫秒内完成

理想情况下,我们希望40个任务正好运行在4个线程上:

  • 少于4个线程无法充分利用所有核心

  • 多于4个线程会浪费内存和CPU时间创建不必要的线程
    而这正是线程池的实际工作方式。使线程数与核心数匹配,既能保持较小的内存占用,又不会影响性能——前提是线程被高效利用(本例即是如此)。

但当每个任务改为查询网络(等待半秒响应且本地CPU闲置)时,线程池的节约策略就会失效。此时创建更多线程让所有网络查询并发执行反而更高效。

为此线程池准备了备用方案:如果任务队列持续半秒未变化,就会以每半秒一个的速度新增线程,直到达到线程池容量上限。

这半秒延迟是把双刃剑:

  • 优势:防止一次性短期活动导致程序突然多占用40MB(或更多)内存

  • 劣势:当线程阻塞时(如数据库查询或调用WebClient.DownloadFile)可能造成不必要延迟

因此可以通过SetMinThreads告知线程池不要延迟创建前x个线程,例如:

//(第二个参数指定分配给I/O完成端口的线程数,该机制用于异步编程模型)
ThreadPool.SetMinThreads (50, 50);

默认值为每个处理器核心1个线程。


本小节完......

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/87358.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/87358.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

学习笔记(29):训练集与测试集划分详解:train_test_split 函数深度解析

学习笔记(29):训练集与测试集划分详解&#xff1a;train_test_split 函数深度解析 一、为什么需要划分训练集和测试集&#xff1f; 在机器学习中&#xff0c;模型需要经历两个核心阶段&#xff1a; 训练阶段&#xff1a;用训练集数据学习特征与目标值的映射关系&#xff08;…

【全网唯一】自动化编辑器 Windows版纯本地离线文字识别插件

目的 自动化编辑器超轻量级RPA工具&#xff0c;零代码制作RPA自动化任务&#xff0c;解放双手&#xff0c;释放双眼&#xff0c;轻松玩游戏&#xff0c;刷任务。本篇文章主要讲解下自动化编辑器的TomatoOCR纯本地离线文字识别Windows版插件如何使用和集成。 准备工作 1、下载自…

GitHub 2FA绑定

GitHub 2FA绑定 作为全球最大的代码托管平台&#xff0c;GitHub对账号安全的重视程度不断提升——自2023年3月起&#xff0c;GitHub已要求所有在GitHub.com上贡献代码的用户必须启用双因素身份验证&#xff08;2FA&#xff09;。如果你是符合条件的用户&#xff0c;会收到一封…

pytest fixture基础大全详解

一、介绍 作用 fixture主要有两个作用&#xff1a; 复用测试数据和环境&#xff0c;可以减少重复的代码&#xff1b;可以在测试用例运行前和运行后设置和清理资源&#xff0c;避免对测试结果产生影响&#xff0c;同时也可以提高测试用例的运行效率。 优势 pytest框架的fix…

Unity知识点-Renderer常用材质变量

本篇总结了Unity中renderer的3种常用的材质相关的变量&#xff1a;renderer.material,renderer.sharedMaterial,renderer.MaterialPropertyBlock。以及三者对SRPBatcher的影响。 一.介绍及对比 1.概念介绍 1.material 定义&#xff1a;material 是Render组件&#xff08;如…

【算法】​​如何判断时间复杂度?

文章目录 1. 什么是时间复杂度&#xff1f;为什么需要时间复杂度&#xff1f; 2. 常见时间复杂度对比3. 如何分析时间复杂度&#xff1f;&#xff08;Java版&#xff09;&#x1f539; 步骤1&#xff1a;找出基本操作&#x1f539; 步骤2&#xff1a;分析循环结构&#xff08;1…

MySQL使用C语言连接

文章目录 版本查看以及编译mysql接口介绍初始化链接数据库下发mysql命令mysql_query获取执行结果mysql_store_result获取结果行数mysql_num_rows获取结果列数mysql_num_fields获取列名mysql_fetch_fields获取结果内容mysql_fetch_row关闭mysql链接mysql_closeC语言操作mysql查看…

坚持每日Codeforces三题挑战:Day 7 - 题目详解(2025-06-11,难度:1200,1300,1500)

每天坚持写三道题第七天&#xff1a; Problem - A - Codeforces 1200 Problem - B - Codeforces 1300 Problem - A - Codeforces 1500 目录 题目一: 题目大意: 解题思路: 代码(C): 题目二: 题目大意: 解题思路: 代码(C): 题目三: 题目大意: 解题思路: 代码(C): …

洛谷 P4305:[JLOI2011] 不重复数字 ← unordered_set

【题目来源】 https://www.luogu.com.cn/problem/P4305 【题目描述】 给定 n 个数&#xff0c;要求把其中重复的去掉&#xff0c;只保留第一次出现的数。 【输入格式】 第一行一个整数 T&#xff0c;表示数据组数。 对于每组数据&#xff0c;第一行一个整数 n。第二行 n 个数…

STM32固件升级设计——SPIFLASH模拟U盘升级固件

目录 概述 一、功能描述 1、BootLoader部分&#xff1a; 2、APP部分&#xff1a; 二、BootLoader程序制作 1、分区定义 2、 主函数 3、配置USB 4、配置fatfs文件系统 5、程序跳转 三、APP程序制作 四、工程配置&#xff08;默认KEIL5&#xff09; 五、运行测试 六…

解锁阿里云日志服务SLS:云时代的日志管理利器

引言&#xff1a;开启日志管理新篇 在云计算时代&#xff0c;数据如同企业的血液&#xff0c;源源不断地产生并流动。从用户的每一次点击&#xff0c;到系统后台的每一个操作&#xff0c;数据都在记录着企业运营的轨迹。而在这些海量的数据中&#xff0c;日志数据占据着至关重…

Keye-VL-8B-Preview:由快手 Kwai Keye 团队精心打造的尖端多模态大语言模型

&#x1f525; News 2025.06.26 &#x1f31f; 我们非常自豪地推出Kwai Keye-VL&#xff0c;这是快手Kwai Keye团队精心打造的前沿多模态大语言模型。作为快手先进技术生态中的核心AI产品&#xff0c;Keye在视频理解、视觉感知和推理任务方面表现卓越&#xff0c;树立了新的性…

Web前端之JavaScript实现图片圆环、圆环元素根据角度指向圆心、translate、rotate

MENU 前言效果HtmlStyleJavaScript 前言 代码段创建了一个由6个WiFi图标组成的圆形排列&#xff0c;每个图标均匀分布在圆周上。 效果 Html 代码 <div class"ring"><div class"item"><img class"img" src"../image/icon/W…

1 Studying《Computer Vision: Algorithms and Applications 2nd Edition》11-15

目录 Chapter 11 Structure from motion and SLAM 11.1 几何内禀校准 11.2 姿态估计 11.3 从运动中获得的双帧结构 11.4 从运动中提取多帧结构 11.5 同步定位与建图&#xff08;SLAM&#xff09; 11.6 额外阅读 Chapter 12 Depth estimation 12.1 极点几何 12.2 稀疏…

phpstudy 可以按照mysql 数据库

phpstudy 可以按照mysql 数据库 PHPStudy&#xff08;小皮面板&#xff09;是一款专为开发者设计的集成环境工具&#xff0c;涵盖服务器配置、开发环境搭建、网站部署等多项功能。以下是其核心用途及优势的详细解析&#xff1a; 一、开发环境快速搭建 一站式集成环境集成Apa…

Python搭建HTTP服务,如何用内网穿透快速远程访问?

Python的内置HTTP服务模块是开发者工具箱中的瑞士军刀&#xff0c;只需一行命令即可启动一个功能完备的Web服务器。无论是前端工程师调试页面、数据科学家共享Jupyter Notebook&#xff0c;还是后端开发者快速验证API原型&#xff0c;Python HTTP服务都能以零配置的方式满足需求…

拨号音识别系统的设计与实现

拨号音识别系统的设计与实现 摘要 本文设计并实现了一个完整的拨号音识别系统&#xff0c;该系统能够自动识别电话号码中的数字。系统基于双音多频(DTMF)技术原理&#xff0c;使用MATLAB开发&#xff0c;包含GUI界面展示处理过程和结果。系统支持从麦克风实时录音或加载音频文…

数据结构-树详解

树简介 树存储和组织具有层级结构的数据&#xff08;例&#xff1a;公司职级&#xff09;&#xff0c;就是一颗倒立生长的树。 属性&#xff1a; 递归n个节点有n-1个连接节点x的深度&#xff1a;节点x到根节点的最长路径节点x的高度&#xff1a;节点x到叶子节点的最长路径 …

【安卓Sensor框架-2】应用注册Sensor 流程

注册传感器的核心流程为如下&#xff1a;应用层调用 SensorManager注册传感器&#xff0c;framework层创建SensorEventQueue对象&#xff08;事件队列&#xff09;&#xff0c;通过JNI调用Native方法nativeEnableSensor()&#xff1b;SensorService服务端createEventQueue()创建…

新版本没有docker-desktop-data分发 | docker desktop 镜像迁移

在新版本的docker desktop中&#xff08;如4.42版本&#xff09;&#xff0c;镜像迁移只需要更改路径即可。如下&#xff1a; 打开docker desktop的设置&#xff08;图1&#xff09;&#xff0c;将图2的原来的地址C:\Users\用户\AppData\Local\Docker\wsl修改为你想要的空文件…