马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
02-并发的底层:Thread、ThreadPool 与 Task 的关系
本章 GitHub 堆栈:csharp-concurrency-cookbook ⭐
接待 Star 和 Fork!全部代码示例都可以在堆栈中找到并运行。
📖 写在前面的话
各位好!👋
先给各人打个防备针:这篇博客的内容真的许多,而且绝大部门都是偏理论的深度剖析。我不会跟你讲"怎么快速用Task",而是会带你深入到利用体系层面,搞清楚Thread、ThreadPool和Task的本质区别和底层机制。
预计阅读时间:30-60分钟(取决于你的底子和阅读速率)
内容密度:💎💎💎💎💎(满分5颗钻,这篇给5颗)
发起:
- ☕ 泡杯咖啡或茶,找个安静的地方
- 📖 不要快速欣赏,许多细节值得反复琢磨
- 💻 对照着代码示例明白,效果最好
- 🤔 看不懂的地方可以先跳过,转头再看
为什么值得你花这个时间?
由于这些知识是真正的硬核干货,搞懂了之后:
- ✅ 你会明确为什么"10000个Thread会瓦解,10000个Task却轻松运行"
- ✅ 你会明白.NET Core线程池相比.NET Framework快4-10倍的缘故原由
- ✅ 你会知道async/await为什么能用几十个线程处置处罚上万个并发哀求
- ✅ 你会把握性能优化的底层原理,而不是"听说要用Task"
这篇文章报告的就是并发编程的底层原理,是后续全部章节的理论底子,是你成为并发编程高手的必经之路。
以是,假如你真的想在并发编程上有所突破,请沉下心,认真看完这篇。包管你不会白花时间! 💪
好了,废话不多说,让我们开始吧!
焦点题目:从 Thread 到 Task,.NET 并发编程履历了怎么演化?它们之间是什么关系?
📌 弁言:一个让人震惊的实行
在第一章的末了,我们留了一个实行:分别创建 10000 个线程和 10000 个 Task,观察它们的差别。
现在让我们来做这个实行,效果大概会让你大吃一惊。
实行 1:创建 10000 个线程
- // 代码示例:ThreadVsTaskDemo.cs
- private static void CreateTenThousandThreads()
- {
- const int threadCount = 10000;
- var sw = Stopwatch.StartNew();
- var threads = new Thread[threadCount];
- Console.WriteLine($"开始创建 {threadCount} 个线程...");
- for (int i = 0; i < threadCount; i++)
- {
- threads[i] = new Thread(() =>
- {
- Thread.Sleep(5000); // 模拟工作 5 秒
- });
- threads[i].Start();
- if ((i + 1) % 1000 == 0)
- {
- Console.WriteLine($" 已创建 {i + 1} 个线程...");
- }
- }
- Console.WriteLine("所有线程已启动,等待完成...");
- foreach (var thread in threads)
- {
- thread.Join();
- }
- sw.Stop();
- Console.WriteLine($"✓ 完成时间:{sw.ElapsedMilliseconds}ms");
- Console.WriteLine($"✓ 创建的线程数:{threadCount} 个");
- Console.WriteLine($"✓ 虚拟内存预留:约 {threadCount / 1024.0:F2} GB(每线程 1MB 栈空间)");
- Console.WriteLine($"✓ 实际物理内存:请查看任务管理器(约 1-2 GB,因为栈未被充分使用)");
- Console.WriteLine();
- Console.WriteLine("说明:");
- Console.WriteLine(" - Windows 为每个线程预留 1MB 虚拟地址空间");
- Console.WriteLine(" - 但只有栈被实际访问时,才分配物理内存(4KB 页)");
- Console.WriteLine(" - 当前代码仅调用 Thread.Sleep(),栈使用很少");
- Console.WriteLine(" - 若要查看虚拟内存占用,请使用 Process Explorer 工具");
- }
复制代码 运行效果:- 开始创建 10000 个线程...
- 已创建 1000 个线程...
- 已创建 2000 个线程...
- ...
- 已创建 10000 个线程...
- 所有线程已启动,等待完成...
- ✓ 完成时间:约 5000-10000ms(取决于系统)
- ✓ 创建的线程数:10000 个
- ✓ 虚拟内存预留:约 9.77 GB(每线程 1MB 栈空间)
- ✓ 实际物理内存:约 1-2 GB(任务管理器显示)
- 说明:
- - Windows 为每个线程预留 1MB 虚拟地址空间
- - 但只有栈被实际访问时,才分配物理内存(4KB 页)
- - 当前代码仅调用 Thread.Sleep(),栈使用很少
- - 若要查看虚拟内存占用,请使用 Process Explorer 工具
复制代码💡 关于内存占用的紧张阐明
你大概会注意到:使命管理器表现的内存只有 1-2GB,而不是理论的 10GB。这是正常征象!
为什么?
- 假造内存(预留):10GB —— 这是利用体系为全部线程栈预留的假造地点空间
- 物理内存(实际):1-2GB —— 这是实际分配的物理内存
关键机制:按需分配(Demand Paging)
- Windows 为每个线程预留 1MB 假造地点空间(VirtualAlloc)
- 但只有当栈空间被实际访问时,才分配物理内存(4KB 页为单位)
- 本实行的线程只调用 Thread.Sleep(),险些不利用栈空间
- 没有局部变量、没有深度函数调用 → 栈利用少少 → 物理内存分配少少
真实场景对比:- 简单场景(本实验):
- - 虚拟:10GB
- - 物理:1-2GB
- - 原因:栈使用极少
- 复杂场景(实际应用):
- - 虚拟:10GB
- - 物理:5-10GB
- - 原因:深度调用栈、大量局部变量
复制代码 本实行的真正重点:
- ✅ 线程数目过多:10000 个 OS 线程的创建和管理资源
- ✅ 上下文切换频仍:大量线程导致 CPU 浪费在调治上
- ⚠️ 内存不是焦点题目:固然假造内存很大,但物理内存斲丧取决于实际利用
要观察假造内存?利用 Process Explorer:
- 下载:Windows Sysinternals - Process Explorer
- 查察列:Virtual Size(假造内存) vs Working Set(物理内存)
其他性能题目:
- CPU 上下文切换:频仍
- 体系相应:卡顿/瓦解
- 线程创建时间:50-200 微秒/线程
实行 2:创建 10000 个 Task
- // 代码示例:ThreadVsTaskDemo.cs
- private static async Task CreateTenThousandTasksAsync()
- {
- const int taskCount = 10000;
- var sw = Stopwatch.StartNew();
- var tasks = new Task[taskCount];
- Console.WriteLine($"开始创建 {taskCount} 个 Task...");
- // 记录初始线程数
- var initialThreadCount = ThreadPool.ThreadCount;
- for (int i = 0; i < taskCount; i++)
- {
- tasks[i] = Task.Run(async () =>
- {
- await Task.Delay(5000); // I/O 密集型:不占用线程
- });
- }
- // 等待一下让任务启动
- await Task.Delay(100);
- // 查看运行时的线程数
- var runningThreadCount = ThreadPool.ThreadCount;
- Console.WriteLine("所有 Task 已启动,等待完成...");
- await Task.WhenAll(tasks);
- sw.Stop();
- Console.WriteLine($"✓ 完成时间:{sw.ElapsedMilliseconds}ms");
- Console.WriteLine($"✓ 实际使用线程数:约 {runningThreadCount}(初始:{initialThreadCount})");
- Console.WriteLine($"✓ Task 对象内存:约 {taskCount * 200 / 1024.0 / 1024.0:F2} MB");
- Console.WriteLine($"✓ 性能优势:线程数减少约 {taskCount / Math.Max(runningThreadCount, 1)}x");
- // 对比说明
- Console.WriteLine($"\n对比分析:");
- Console.WriteLine($" - 如果用 {taskCount} 个 Thread:内存约 {taskCount / 1024.0:F2} GB");
- Console.WriteLine($" - 实际用 Task:内存约 {taskCount * 200 / 1024.0 / 1024.0:F2} MB");
- Console.WriteLine($" - 内存节省:约 {(taskCount / 1024.0 * 1024) / (taskCount * 200 / 1024.0):F0}x");
- }
复制代码 运行效果:- 开始创建 10000 个 Task...
- 所有 Task 已启动,等待完成...
- ✓ 完成时间:约 5000ms
- ✓ 实际使用线程数:约 10-20(初始:4-8)
- ✓ Task 对象内存:约 1.91 MB
- ✓ 性能优势:线程数减少约 500-1000x
- 对比分析:
- - 如果用 10000 个 Thread:内存约 9.77 GB(虚拟内存)
- - 实际用 Task:内存约 1.91 MB(Task 对象)
- - 内存节省:约 5000x
复制代码 🤔 为什么差距云云巨大?
这个实行展现了一个焦点题目:- Thread:
- - 每个 Thread = 1 个 OS 线程
- - 10000 个 Thread = 10000 个 OS 线程
- - 虚拟内存开销:约 10GB(预留)
- - 物理内存开销:1-10GB(取决于实际使用)
- - 上下文切换:频繁(10000 个线程竞争 CPU)
- - 创建/销毁:昂贵(每个线程 50-200 微秒)
- Task:
- - Task ≠ 线程
- - Task = 异步操作的抽象
- - 10000 个 Task 可能只用 10-20 个线程
- - 内存开销:约 2 MB(Task 对象)
- - 上下文切换:少(只有 10-20 个线程)
- - 复用线程池:无创建/销毁成本
复制代码 引发思索:- 1. Thread 和 Task 到底是什么关系?
- 2. ThreadPool 是如何优化线程使用的?
- 3. Task 是如何实现"用少量线程处理大量任务"的?
- 4. 为什么实际物理内存远小于虚拟内存?(按需分配机制)
- - 上下文切换:少
- - 复用线程池
复制代码 带着这些题目,我们开始深入探索 .NET 并发编程的底层机制。
🏗️ Part 1: Thread 的资源与代价
1.1 Thread 的本质
Thread(线程) 是利用体系(OS)级别的实行单位。
线程的底层实现
当你写下 var thread = new Thread(() => { /* work */ }); 时,体系底层究竟发生了什么?
完备流程图:
flowchart TD A[用户代码: var thread = new Thread] --> B[C# 编译器] B --> C[天生 IL 代码] C --> D[运行时: CLR 继承] D --> E[创建 Thread 对象
托管内存分配] E --> F{调用 thread.Start?} F -->|否| G[Thread 对象创建完成
状态: Unstarted] F -->|是| H[CLR 调用 ThreadNative::Start] H --> I[P/Invoke: 调用利用体系 API] I --> J{利用体系范例?} J -->|Windows| K[调用 CreateThread API] J -->|Linux| L[调用 pthread_create] K --> M[Windows 内核处置处罚] L --> M M --> N[分配内核线程对象
KTHREAD 布局] N --> O[分配用户栈空间
默认 1 MB] O --> P[创建 TEB
Thread Environment Block] P --> Q[初始化线程上下文
寄存器、栈指针等] Q --> R[线程到场调治器队列] R --> S[返回线程句柄给 CLR] S --> T[CLR 生存线程句柄] T --> U[线程状态: Running
等待 OS 调治] U --> V[OS 调治器分配 CPU 时间片] V --> W[线程开始实行用户代码] style A fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px style W fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style M fill:#fff9c4,color:#f57f17,stroke:#f9a825,stroke-width:2px style E fill:#e1bee7,color:#6a1b9a,stroke:#7b1fa2,stroke-width:2px style O fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px详细步调剖析:
第一阶段:托管层(CLR)
- // 用户代码
- var thread = new Thread(() =>
- {
- Console.WriteLine("Hello from thread!");
- });
- // 对应的底层操作:
- // 1. CLR 分配 Thread 对象(托管堆)
- // 2. 初始化 Thread 对象的字段:
- // - m_Delegate(要执行的委托)
- // - m_ThreadStart(线程入口点)
- // - m_ThreadId(初始为 0)
- // - m_ThreadState(Unstarted)
复制代码 第二阶段:启动线程
- thread.Start(); // ← 这一步才真正创建 OS 线程
- // 底层流程:
- // CLR 内部调用栈:
- Thread.Start()
- └─ StartInternal()
- └─ ThreadNative::Start() // C++ 实现
- └─ P/Invoke
- └─ CreateThread (Windows)
- └─ pthread_create (Linux)
复制代码 第三阶段:利用体系层
Windows 体系:- // 伪代码:操作系统内核做了什么
- HANDLE CreateThread(
- LPSECURITY_ATTRIBUTES lpThreadAttributes,
- SIZE_T dwStackSize, // 栈大小,默认 1 MB
- LPTHREAD_START_ROUTINE lpStartAddress,
- LPVOID lpParameter,
- DWORD dwCreationFlags,
- LPDWORD lpThreadId
- )
- {
- // 1. 分配内核线程对象(KTHREAD)
- PKTHREAD kthread = AllocateKernelThreadObject();
- // 2. 分配用户态栈空间(默认 1 MB)
- PVOID stackBase = VirtualAlloc(NULL, 1 * 1024 * 1024, ...);
- // 3. 创建线程环境块(TEB)
- PTEB teb = CreateThreadEnvironmentBlock();
- teb->StackBase = stackBase;
- teb->StackLimit = stackBase + stackSize;
- // 4. 初始化线程上下文(寄存器、栈指针等)
- CONTEXT context;
- InitializeContext(&context, lpStartAddress, stackBase);
- // 5. 将线程加入调度器的就绪队列
- KeReadyThread(kthread);
- // 6. 返回线程句柄
- return CreateHandle(kthread);
- }
复制代码 Linux 体系:- // 伪代码:pthread_create 的内部实现
- int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
- void *(*start_routine) (void *), void *arg)
- {
- // 1. 分配线程描述符(task_struct)
- struct task_struct *new_task = alloc_task_struct();
- // 2. 分配栈空间(默认 8 MB,可配置)
- void *stack = mmap(NULL, PTHREAD_STACK_MIN, ...);
- // 3. 复制父线程的资源(文件描述符、信号等)
- copy_process(new_task, CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND);
- // 4. 设置线程入口点和参数
- new_task->thread.sp = (unsigned long)stack + PTHREAD_STACK_MIN;
- new_task->thread.ip = (unsigned long)start_routine;
- // 5. 唤醒线程(加入调度队列)
- wake_up_new_task(new_task);
- return 0;
- }
复制代码 第四阶段:内存分配详情
每个线程的内存占用:
flowchart TB subgraph Thread["线程内存布局 (总计:1-8MB)"] direction TB Stack["用户态栈 Stack
━━━━━━━━━━━━━━━━
巨细: 1 MB Windows 默认
8 MB Linux 默认
━━━━━━━━━━━━━━━━
用途: 存储局部变量和函数调用栈"] TEB["TEB / TLS
Thread Environment Block
━━━━━━━━━━━━━━━━
巨细: 约 4 KB
━━━━━━━━━━━━━━━━
用途: 线程局部存储"] Kernel["内核线程对象
KTHREAD / task_struct
━━━━━━━━━━━━━━━━
巨细: 约几 KB
位置: 内核空间
━━━━━━━━━━━━━━━━
用途: 线程元数据、调治信息"] Stack --> TEB TEB --> Kernel end style Stack fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px style TEB fill:#fff9c4,color:#f57f17,stroke:#f9a825,stroke-width:2px style Kernel fill:#e1bee7,color:#6a1b9a,stroke:#7b1fa2,stroke-width:2px style Thread fill:#e0e0e0,color:#424242,stroke:#616161,stroke-width:2px第五阶段:线程调治
- 时间轴:
- 0 ms: thread.Start() 调用
- ↓
- 5 ms: OS 创建线程对象完成
- ↓
- 10 ms: 线程进入就绪队列
- ↓
- ???: 等待 CPU 时间片(取决于系统负载)
- ↓
- 15 ms: OS 调度器分配 CPU
- ↓
- 15 ms: 线程开始执行用户代码
复制代码 关键点:
- ⚠️ new Thread() 只是创建托管对象,不斲丧 OS 资源
- ⚠️ thread.Start() 才真正创建 OS 线程,斲丧 1-8 MB 内存
- ⚠️ 线程创建后不会立刻实行,须要等待 OS 调治
Thread 的内存开销
每个线程的内存占用(假造 vs 物理):
构成部门假造内存(预留)物理内存(实际利用)阐明栈空间Windows: 1 MB
Linux: 8 MB取决于实际利用
通常 4-16 KB(简单代码)
最多靠近假造巨细(深度递归)用于存储局部变量和调用栈
按需分配(Demand Paging)TEB / TLS几 KB几 KB线程环境块(Thread Environment Block)内查对象几 KB几 KB内核中的线程布局总计约 1-8 MB通常 8-32 KB
(取决于代码复杂度)假造内存是预留,物理内存是实际分配关键概念:假造内存 vs 物理内存- 虚拟内存(Virtual Memory):
- - 操作系统为线程栈预留的地址空间
- - Windows:VirtualAlloc() 预留 1 MB
- - Linux:mmap() 预留 8 MB
- - 不占用物理 RAM,仅占用地址空间
- 物理内存(Physical Memory / RAM):
- - 实际分配的 RAM 页(4KB 为单位)
- - 只有当栈被访问时才分配(Demand Paging)
- - 简单代码(如 Thread.Sleep)仅使用几 KB
- - 复杂代码(深度递归、大量局部变量)可能使用接近 1 MB
复制代码 盘算示例(假造内存预留):- Windows(每个线程预留 1 MB 栈空间):
- 1000 个线程 = 1000 MB ≈ 1 GB(虚拟内存)
- 10000 个线程 = 10000 MB ≈ 10 GB(虚拟内存)
- 但物理内存:
- - 简单代码:1000 个线程 ≈ 8-16 MB
- - 复杂代码:1000 个线程 ≈ 500 MB - 1 GB
- Linux(每个线程预留 8 MB 栈空间):
- 1000 个线程 = 8000 MB ≈ 8 GB(虚拟内存)
- 10000 个线程 = 80000 MB ≈ 78 GB(虚拟内存)
- 但物理内存类似 Windows(取决于实际使用)
复制代码💡 为什么会有这个差别?
利用体系利用按需分配(Demand Paging)战略:
- 创建线程时,预留假造地点空间(VirtualAlloc/mmap)
- 栈空间被访问时,才分配物理内存(4KB 页)
- 本章开头实行中的 Thread.Sleep() 险些不利用栈 → 物理内存很少
- 实际应用中的深度调用栈会利用更多物理内存
观察方法:
- 使命管理器:表现工作集(Working Set) = 物理内存
- Process Explorer:可查察假造巨细(Virtual Size) = 假造内存
1.2 Thread 的上下文切换资源
什么是上下文切换?
上下文切换(Context Switch) 是指 CPU 从一个线程切换到另一个线程的过程。这是多线程并发的底子,但也是性能开销的重要泉源。
简化流程:- 线程 A 正在运行
- ↓
- 时间片用完(或 I/O 阻塞)
- ↓
- 保存 A 的状态(寄存器、栈指针、PC)
- ↓
- 加载线程 B 的状态
- ↓
- 线程 B 开始运行
复制代码 但实际的底层流程远比这复杂得多。
上下文切换的完备流程
sequenceDiagram participant A as 线程 A participant CPU as CPU participant OS as OS 内核 participant Mem as 内存 participant B as 线程 B Note over A,CPU: 线程 A 正在实行 A->>CPU: 实行指令 CPU->>CPU: 时间片用完 / I/O 壅闭 Note over CPU,OS: 第一阶段:生存上下文 CPU->>OS: 触发上下文切换 OS->>OS: 进入内核态 OS->>Mem: 生存线程 A 的寄存器
(PC, SP, 通用寄存器) OS->>Mem: 生存线程 A 的栈指针 OS->>Mem: 生存线程 A 的 CPU 状态 Note over OS,Mem: 第二阶段:选择下一个线程 OS->>OS: 运行调治算法
(选择线程 B) OS->>Mem: 加载线程 B 的 PCB
(进程控制块) Note over OS,CPU: 第三阶段:加载新上下文 OS->>Mem: 从内存读取线程 B 的寄存器 OS->>CPU: 规复线程 B 的 PC OS->>CPU: 规复线程 B 的 SP OS->>CPU: 规复线程 B 的通用寄存器 OS->>CPU: 切换到用户态 Note over CPU,B: 线程 B 开始实行 CPU->>B: 实行指令为什么上下文切换云云昂贵?
1. 直接时间资源
须要生存和规复的内容:
组件内容数目步伐计数器(PC)下一条指令的地点1 个栈指针(SP)当前栈的位置1 个通用寄存器盘算数据16-32 个(x64)浮点寄存器浮点运算数据16 个(x64)SIMD 寄存器向量运算数据16 个(AVX)段寄存器内存分段信息6 个标记寄存器CPU 状态标记1 个TLB页表缓存数百条时间分解:- 保存寄存器状态: 约 0.5-1 微秒
- 运行调度算法: 约 0.5-2 微秒
- 加载新线程状态: 约 0.5-1 微秒
- 刷新 TLB: 约 0.5-1 微秒
- 进入/退出内核态: 约 0.5-1 微秒
- ────────────────────────────────────
- 总计: 约 2-7 微秒
复制代码对比:一个简单的 CPU 指令实行时间约 1 纳秒,上下文切换相当于实行 2000-7000 条 CPU 指令!
2. CPU 缓存失效(Cache Miss)
这是上下文切换最大的隐性资源。
CPU 缓存条理布局:
flowchart TB subgraph Core["CPU 焦点"] direction TB L1["L1 缓存
━━━━━━━━━━━━
巨细: 32-64 KB
延伸: 约 4 个时钟周期
数据 + 指令"] L2["L2 缓存
━━━━━━━━━━━━
巨细: 256 KB - 1 MB
延伸: 约 12 个时钟周期
私有缓存"] L1 --> L2 end L3["L3 缓存
━━━━━━━━━━━━
巨细: 8-32 MB
延伸: 约 40 个时钟周期
共享缓存"] RAM["主内存 RAM
━━━━━━━━━━━━
巨细: 8-128 GB
延伸: 约 200 个时钟周期
DRAM"] Core --> L3 L3 --> RAM style Core fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px style L1 fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style L2 fill:#fff9c4,color:#f57f17,stroke:#f9a825,stroke-width:2px style L3 fill:#ffe0b2,color:#e65100,stroke:#ef6c00,stroke-width:2px style RAM fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px上下文切换导致的缓存失效:
flowchart TD A[线程 A 实行] --> B[L1/L2/L3 缓存
存储线程 A 的数据] B --> C[时间片用完
上下文切换] C --> D[切换到线程 B] D --> E[线程 B 访问数据] E --> F{数据在缓存中?} F -->|否| G[Cache Miss!
从内存加载] F -->|是| H[Cache Hit
快速访问] G --> I[延伸:约 200 个时钟周期] H --> J[延伸:约 4 个时钟周期] style G fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px style I fill:#ef9a9a,color:#c62828,stroke:#d32f2f,stroke-width:2px style H fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style J fill:#a5d6a7,color:#1b5e20,stroke:#2e7d32,stroke-width:2px性能影响:- 假设:
- - CPU 频率:3 GHz(每时钟周期约 0.33 纳秒)
- - L1 缓存命中率(无上下文切换):95%
- - L1 缓存命中率(频繁上下文切换):50%
- 无上下文切换:
- 95% × 4 周期 + 5% × 200 周期 = 13.8 周期 ≈ 4.6 纳秒
- 频繁上下文切换:
- 50% × 4 周期 + 50% × 200 周期 = 102 周期 ≈ 34 纳秒
- 性能下降:34 / 4.6 ≈ 7.4 倍!
复制代码 3. TLB(Translation Lookaside Buffer)失效
TLB 的作用:
- 缓存假造地点到物理地点的映射
- 克制每次内存访问都查页表
上下文切换的影响:- 线程 A → 线程 B
- ↓
- TLB 中存储的是线程 A 的地址映射
- ↓
- 线程 B 需要不同的地址映射
- ↓
- TLB 失效(Flush)
- ↓
- 线程 B 的每次内存访问都需要查页表
- ↓
- 延迟增加:从 1 个时钟周期 → 10-100 个时钟周期
复制代码 4. 指令流水线停顿
CPU 流水线:- 指令 1: 取指 → 解码 → 执行 → 访存 → 写回
- 指令 2: 取指 → 解码 → 执行 → 访存 → 写回
- 指令 3: 取指 → 解码 → 执行 → 访存 → 写回
复制代码 上下文切换导致流水线清空:- 线程 A 执行中:
- 流水线:[指令5][指令4][指令3][指令2][指令1]
- ↓
- 上下文切换
- ↓
- 流水线清空:[ ][ ][ ][ ][ ]
- ↓
- 线程 B 开始:
- 重新填充流水线:[指令1][ ][ ][ ][ ]
- ↓
- 浪费:约 10-20 个时钟周期
复制代码 上下文切换的总资源
完备资源分解:
资源范例直接资源间接资源总计寄存器生存/规复约 2 微秒-2 微秒L1 缓存失效-约 10-50 微秒10-50 微秒L2 缓存失效-约 20-100 微秒20-100 微秒TLB 失效-约 5-20 微秒5-20 微秒流水线停顿-约 0.01-0.1 微秒0.01-0.1 微秒调治算法约 1 微秒-1 微秒总资源约 3 微秒约 35-170 微秒38-173 微秒关键洞察:直接资源只占总资源的约 5-10%,间接资源(缓存失效)才是重要开销!
实行:丈量上下文切换资源
- // 代码示例:ContextSwitchCostDemo.cs
- static void MeasureContextSwitchCost()
- {
- const int iterations = 1_000_000;
-
- // 单线程基准测试
- var sw1 = Stopwatch.StartNew();
- for (int i = 0; i < iterations; i++)
- {
- // 简单计算
- var result = i * i;
- }
- sw1.Stop();
- Console.WriteLine($"单线程:{sw1.ElapsedMilliseconds}ms");
-
- // 多线程(强制上下文切换)
- var sw2 = Stopwatch.StartNew();
- var threads = new Thread[10];
- for (int i = 0; i < 10; i++)
- {
- threads[i] = new Thread(() =>
- {
- for (int j = 0; j < iterations / 10; j++)
- {
- var result = j * j;
- Thread.Sleep(0); // 强制上下文切换
- }
- });
- threads[i].Start();
- }
- foreach (var t in threads) t.Join();
- sw2.Stop();
- Console.WriteLine($"多线程(频繁切换):{sw2.ElapsedMilliseconds}ms");
- }
复制代码 预期效果:- 单线程:约 5ms
- 多线程(频繁切换):约 500ms(慢 100 倍!)
复制代码 结论:
- ⚠️ 上下文切换的直接资源:2-7 微秒
- ⚠️ 上下文切换的间接资源:35-170 微秒(缓存失效)
- ⚠️ 总资源:相当于 38000-173000 条 CPU 指令!
False Sharing(伪共享):多线程的隐蔽杀手
False Sharing 是多线程编程中最轻易被忽视的性能杀手之一。它发生在多个线程访问同一 缓存行(Cache Line) 的差别数据时。
什么是缓存行?
CPU 缓存不是按单个字节读取的,而是按 缓存行(Cache Line) 为单位,通常是 64 字节(x86/x64 架构的典范值,ARM 架构大概为 32、64 或 128 字节)。- 缓存行结构(x86/x64 典型值:64 字节):
- ┌────────────────────────────────────────────────────┐
- │ 字节 0-7 │ 字节 8-15 │ ... │ 字节 56-63 │
- │ 数据 A │ 数据 B │ ... │ 数据 H │
- └────────────────────────────────────────────────────┘
复制代码 False Sharing 的原理
sequenceDiagram participant T1 as 线程 1
(CPU 焦点 1) participant T2 as 线程 2
(CPU 焦点 2) participant Cache1 as L1 缓存 1 participant Cache2 as L2 缓存 2 participant Mem as 主内存 Note over T1,Mem: 初始状态:缓存行共享 T1->>Cache1: 读取数据 A
(缓存行的前 8 字节) Cache1->>Mem: 加载整个缓存行
(64 字节) T2->>Cache2: 读取数据 B
(缓存行的后 8 字节) Cache2->>Mem: 加载整个缓存行
(64 字节) Note over T1,Cache2: 两个缓存都有雷同的缓存行 Note over T1,Mem: 线程 1 修改数据 A T1->>Cache1: 写入数据 A Cache1->>Cache1: 标记缓存举动 Modified Note over Cache1,Cache2: 缓存同等性协议触发 Cache1-->>Cache2: 使 Cache2 中的缓存行失效! Note over T2,Mem: 线程 2 读取数据 B(差别的数据!) T2->>Cache2: 读取数据 B Cache2->>Cache2: Cache Miss!(缓存行失效) Cache2->>Mem: 重新加载整个缓存行
延伸 ~200 个时钟周期 Note over T1,T2: 只管修改的是差别数据
但由于在同一缓存行
导致另一个线程的缓存失效关键题目:
- 线程 1 修改数据 A
- 线程 2 访问数据 B
- 数据 A 和 B 完全差别,但在同一缓存行
- 修改 A 导致整个缓存行失效
- 线程 2 的缓存被迫重新加载(Cache Miss)
实行:False Sharing 的性能影响
- // 代码示例:FalseSharingDemo.cs
- // ❌ 有 False Sharing 的版本
- class BadCounter
- {
- public long Counter1; // 8 字节
- public long Counter2; // 8 字节(在同一缓存行中!)
- }
- static void DemonstrateFalseSharing()
- {
- var bad = new BadCounter();
- var sw = Stopwatch.StartNew();
- var t1 = new Thread(() =>
- {
- for (int i = 0; i < 100_000_000; i++)
- {
- bad.Counter1++; // 线程 1 修改 Counter1
- }
- });
- var t2 = new Thread(() =>
- {
- for (int i = 0; i < 100_000_000; i++)
- {
- bad.Counter2++; // 线程 2 修改 Counter2
- }
- });
- t1.Start();
- t2.Start();
- t1.Join();
- t2.Join();
- sw.Stop();
- Console.WriteLine($"有 False Sharing:{sw.ElapsedMilliseconds}ms");
- }
- // ✅ 避免 False Sharing 的版本
- [StructLayout(LayoutKind.Explicit)]
- class GoodCounter
- {
- [FieldOffset(0)]
- public long Counter1; // 偏移 0
- // 填充 56 字节(64 - 8 = 56)
- [FieldOffset(64)]
- public long Counter2; // 偏移 64(在下一个缓存行)
- }
- static void DemonstrateNoPadding()
- {
- var good = new GoodCounter();
- var sw = Stopwatch.StartNew();
- var t1 = new Thread(() =>
- {
- for (int i = 0; i < 100_000_000; i++)
- {
- good.Counter1++;
- }
- });
- var t2 = new Thread(() =>
- {
- for (int i = 0; i < 100_000_000; i++)
- {
- good.Counter2++;
- }
- });
- t1.Start();
- t2.Start();
- t1.Join();
- t2.Join();
- sw.Stop();
- Console.WriteLine($"无 False Sharing:{sw.ElapsedMilliseconds}ms");
- }
复制代码 实际运行效果:- 有 False Sharing:约 8000ms
- 无 False Sharing:约 800ms
- 性能提升:10 倍!
复制代码 False Sharing 的办理方案
方案 1:添补(Padding)- class PaddedCounter
- {
- public long Counter1;
- // 填充 56 字节,确保 Counter2 在下一个缓存行
- private long _padding1, _padding2, _padding3, _padding4, _padding5, _padding6, _padding7;
- public long Counter2;
- }
复制代码 方案 2:利用 [StructLayout] 特性- [StructLayout(LayoutKind.Explicit, Size = 128)] // 两个缓存行
- struct CacheLine
- {
- [FieldOffset(0)]
- public long Counter1;
- [FieldOffset(64)] // 64 字节后,确保在下一个缓存行
- public long Counter2;
- }
复制代码 方案 3:利用线程本地存储(Thread-Local Storage)- class ThreadLocalCounter
- {
- private ThreadLocal<long> _counter = new(() => 0);
- public void Increment()
- {
- _counter.Value++;
- }
- public long GetTotal()
- {
- // 最后合并所有线程的计数
- return _counter.Values.Sum();
- }
- }
复制代码 实际应用中的 False Sharing 案例
案例 1:数组元素访问- // ❌ 错误:多个线程修改相邻数组元素
- long[] counters = new long[4];
- Parallel.For(0, 4, i =>
- {
- for (int j = 0; j < 1000000; j++)
- {
- counters[i]++; // False Sharing!
- }
- });
- // ✅ 正确:使用填充
- long[] counters = new long[4 * 8]; // 每个计数器占 8 个 long(64 字节)
- Parallel.For(0, 4, i =>
- {
- for (int j = 0; j < 1000000; j++)
- {
- counters[i * 8]++; // 每个计数器相隔 64 字节
- }
- });
复制代码 案例 2:多线程计数器- // .NET 的 Interlocked 类已经考虑了 False Sharing
- // 但自定义计数器需要注意
- // ❌ 错误
- class MultiCounter
- {
- public int Count1;
- public int Count2;
- public int Count3;
- public int Count4;
- }
- // ✅ 正确
- [StructLayout(LayoutKind.Explicit)]
- class PaddedMultiCounter
- {
- [FieldOffset(0)]
- public int Count1;
- [FieldOffset(64)]
- public int Count2;
- [FieldOffset(128)]
- public int Count3;
- [FieldOffset(192)]
- public int Count4;
- }
复制代码 常见导致上下文切换的场景
相识哪些利用会触发上下文切换,可以资助我们编写更高效的多线程代码。
1. 壅闭 I/O 利用
- // ❌ 阻塞式 I/O:导致上下文切换
- void BlockingIo()
- {
- // 线程阻塞,等待网络响应
- var response = httpClient.GetStringAsync(url).Result;
- // 线程阻塞,等待文件读取
- var data = File.ReadAllText(filePath);
- }
- // ✅ 异步 I/O:不阻塞线程
- async Task AsyncIo()
- {
- // 线程释放,不阻塞
- var response = await httpClient.GetStringAsync(url);
- // 线程释放,不阻塞
- var data = await File.ReadAllTextAsync(filePath);
- }
- // 性能影响:
- // - 阻塞 I/O:线程等待期间发生上下文切换(浪费线程资源)
- // - 异步 I/O:线程立即释放,可以处理其他任务
复制代码 2. 锁竞争(Lock Contention)
- // ❌ 高锁竞争:频繁上下文切换
- object _lock = new();
- void HighContention()
- {
- // 100 个线程竞争同一个锁
- Parallel.For(0, 100, i =>
- {
- lock (_lock) // 大部分线程会阻塞,等待锁释放
- {
- // 临界区
- Thread.Sleep(10); // 模拟工作
- }
- });
- }
- // ✅ 减少锁粒度:降低竞争
- class ShardedLock
- {
- private readonly object[] _locks = new object[16];
- public ShardedLock()
- {
- for (int i = 0; i < _locks.Length; i++)
- {
- _locks[i] = new object();
- }
- }
- public void Execute(int id, Action action)
- {
- // 根据 ID 选择不同的锁(减少竞争)
- var lockIndex = id % _locks.Length;
- lock (_locks[lockIndex])
- {
- action();
- }
- }
- }
- // 性能影响:
- // - 高竞争:大量线程阻塞 → 频繁上下文切换 → 性能下降 50-90%
- // - 分片锁:竞争减少 → 上下文切换减少 → 性能提升 5-10 倍
复制代码 3. Thread.Sleep() 和 Thread.Yield()
- // ❌ Thread.Sleep(0):主动触发上下文切换
- void ForceContextSwitch()
- {
- for (int i = 0; i < 1000000; i++)
- {
- DoWork();
- Thread.Sleep(0); // 强制上下文切换
- }
- }
- // ⚠️ Thread.Sleep(n):线程休眠,触发上下文切换
- void SleepExample()
- {
- Thread.Sleep(100); // 线程休眠 100ms,让出 CPU
- }
- // ⚠️ Thread.Yield():主动让出 CPU 时间片
- void YieldExample()
- {
- Thread.Yield(); // 让出当前时间片给其他线程
- }
- // 性能影响:
- // - 频繁 Sleep(0):每次调用都触发上下文切换
- // - 实验结果:见上文 ContextSwitchCostDemo(慢 100 倍)
复制代码 4. 等待利用(Wait / Join)
- // ❌ 同步等待:线程阻塞
- void SyncWait()
- {
- var task = Task.Run(() => LongRunningWork());
- task.Wait(); // 线程阻塞,触发上下文切换
- }
- void ThreadJoin()
- {
- var thread = new Thread(() => LongRunningWork());
- thread.Start();
- thread.Join(); // 线程阻塞,触发上下文切换
- }
- // ✅ 异步等待:不阻塞线程
- async Task AsyncWait()
- {
- var task = Task.Run(() => LongRunningWork());
- await task; // 不阻塞线程,不触发上下文切换
- }
- // 性能影响:
- // - task.Wait():阻塞当前线程 → 上下文切换 → 浪费线程资源
- // - await task:释放当前线程 → 无上下文切换 → 高效利用线程池
复制代码 5. 信号量和事故等待
- // ⚠️ ManualResetEvent / AutoResetEvent:阻塞式等待
- void EventWait()
- {
- var resetEvent = new ManualResetEvent(false);
- var t1 = new Thread(() =>
- {
- resetEvent.WaitOne(); // 线程阻塞,等待信号
- DoWork();
- });
- t1.Start();
- Thread.Sleep(1000);
- resetEvent.Set(); // 唤醒等待的线程(触发上下文切换)
- }
- // ✅ SemaphoreSlim.WaitAsync():异步等待
- async Task SemaphoreWaitAsync()
- {
- var semaphore = new SemaphoreSlim(0);
- var task = Task.Run(async () =>
- {
- await semaphore.WaitAsync(); // 不阻塞线程
- DoWork();
- });
- await Task.Delay(1000);
- semaphore.Release(); // 释放信号
- }
- // 性能影响:
- // - WaitOne():阻塞 → 上下文切换(约 2-7 微秒)
- // - WaitAsync():不阻塞 → 无上下文切换
复制代码 6. 线程数目过多
- // ❌ 过多线程:频繁上下文切换
- void TooManyThreads()
- {
- for (int i = 0; i < 1000; i++)
- {
- new Thread(() =>
- {
- while (true)
- {
- DoWork();
- Thread.Sleep(10);
- }
- }).Start();
- }
- }
- // ✅ 使用线程池:线程数量受控
- void UseThreadPool()
- {
- for (int i = 0; i < 1000; i++)
- {
- ThreadPool.QueueUserWorkItem(_ =>
- {
- DoWork();
- });
- }
- }
- // 性能影响:
- // - 1000 个线程 × 100 次切换/秒 = 100000 次上下文切换/秒
- // - 每次切换 50 微秒 = 5 秒/秒(50% 时间在切换!)
复制代码 7. 频仍的 Task.Run
- // ❌ 过度使用 Task.Run:增加调度开销
- async Task OveruseTaskRun()
- {
- for (int i = 0; i < 10000; i++)
- {
- await Task.Run(() => DoSmallWork()); // 小任务,频繁调度
- }
- }
- // ✅ 批量处理:减少调度次数
- async Task BatchProcessing()
- {
- var tasks = Enumerable.Range(0, 10000)
- .Select(i => Task.Run(() => DoSmallWork()))
- .ToArray();
- await Task.WhenAll(tasks);
- }
- // 或者:直接在当前线程执行(如果是 CPU 密集型)
- void DirectExecution()
- {
- for (int i = 0; i < 10000; i++)
- {
- DoSmallWork(); // 如果工作很小,不值得调度
- }
- }
- // 性能影响:
- // - 每次 Task.Run:约 1-5 微秒调度开销
- // - 10000 次 = 10-50ms 开销
复制代码 克制上下文切换的最佳实践
总结表格:
场景题目办理方案性能提升壅闭 I/O线程等待期间壅闭利用 async/await线程利用率提升 10-100 倍锁竞争多线程竞争同一锁分片锁 / 无锁数据布局吞吐量提升 5-10 倍过多线程频仍上下文切换利用线程池(ThreadPool)镌汰 50-90% 上下文切换频仍 Sleep自动触发切换克制 Sleep(0)性能提升 10-100 倍同步等待壅闭线程利用异步方法(WaitAsync)线程利用率提升 5-10 倍False Sharing缓存行伪共享添补 / 线程本地存储性能提升 5-10 倍小使命调治调治开销过大批量处置处罚 / 直接实行镌汰 50-90% 调治开销关键原则:
- ✅ I/O 麋集型:利用 async/await(不占用线程)
- ✅ CPU 麋集型:利用 ThreadPool / Task.Run(线程复用)
- ✅ 克制壅闭:永久不要在异步方法中利用 .Result 或 .Wait()
- ✅ 镌汰锁粒度:利用细粒度锁或无锁数据布局
- ✅ 控制线程数:不要无穷创建线程
- ✅ 克制 False Sharing:利用添补或线程本地存储
1.3 为什么不能无穷创建线程?
限定 1:内存限定
- 假设:
- - 机器内存:16 GB
- - 每个线程:1 MB
- 理论上限:16 GB / 1 MB = 16000 个线程
- 实际上限:约 2000-5000 个(OS 保留、其他进程占用)
复制代码 限定 2:调治器性能降落
- 线程数量 → 调度开销
- 100 个线程:调度开销 < 1%
- 1000 个线程:调度开销 约 5-10%
- 10000 个线程:调度开销 > 50%(大部分时间在切换!)
复制代码 限定 3:饥饿题目
- // 反例:线程饥饿
- for (int i = 0; i < 10000; i++)
- {
- new Thread(() =>
- {
- // 每个线程都想获取同一个锁
- lock (sharedLock)
- {
- // 临界区
- }
- }).Start();
- }
- // 问题:
- // - 大量线程竞争锁
- // - 大部分线程处于等待状态
- // - CPU 浪费在上下文切换上
复制代码 1.4 Thread 的实用场景
只管有这些限定,Thread 仍然有其用武之地:
场景是否得当 Thread短期使命(< 100ms)❌ 不得当(创建资源高)恒久背景使命✅ 得当(如监控 线程)I/O 麋集型❌ 不得当(壅闭线程浪费)CPU 麋集型(大量)❌ 不得当(应利用线程池)须要准确控制线程✅ 得当(如设置优先级、Apartment)小结:
Thread 是强盛但昂贵的资源。在当代 .NET 开发中,应该优先利用 ThreadPool 或 Task,只在须要时利用 Thread。
🔄 Part 2: ThreadPool 的精妙操持
2.1 ThreadPool 的焦点头脑
题目:创建/烧毁线程太昂贵
办理方案:线程池(Thread Pool)- 核心思想:
- 1. 预先创建一组线程
- 2. 任务来了,从池中取一个线程执行
- 3. 任务完成后,线程归还池中(不销毁)
- 4. 复用线程,避免频繁创建/销毁
复制代码 2.2 .NET Framework vs .NET Core 的架构对比
.NET Framework 4.x 的 ThreadPool
特点:
- ✅ 全局工作队列(FIFO)
- ❌ 性能瓶颈:全部线程竞争同一个全局队列锁
- ❌ 没有工作盗取:空闲线程只能等待
- ❌ 负载不均衡:某些线程繁忙,某些线程空闲
架构图:
flowchart TD A[用户代码] --> B[ThreadPool.QueueUserWorkItem] B --> C[全局队列
有锁] C --> D[工作线程 1] C --> E[工作线程 2] C --> F[工作线程 3] C --> G[工作线程 N] D --> H[实行使命] E --> I[实行使命] F --> J[实行使命] G --> K[实行使命] H --> C I --> C J --> C K --> C style C fill:#ffccbc,color:#b71c1c,stroke:#d32f2f,stroke-width:3px Note1[⚠️ 全部线程竞争同一个锁
高并发时性能瓶颈]性能题目表现:
sequenceDiagram participant T1 as 线程 1 participant T2 as 线程 2 participant T3 as 线程 3 participant Q as 全局队列
(有锁) Note over T1,Q: 多个线程同时实行获取使命 T1->>Q: 实行获取锁 T2->>Q: 实行获取锁 T3->>Q: 实行获取锁 Q-->>T1: ✅ 获取锁乐成 Q-->>T2: ❌ 壅闭等待 Q-->>T3: ❌ 壅闭等待 T1->>Q: 取出使命 A T1->>Q: 开释锁 Q-->>T2: ✅ 获取锁乐成 Q-->>T3: ❌ 继续等待 Note over T2,T3: 频仍的锁竞争
低沉吞吐量.NET Core / .NET 5+ 的 ThreadPool
特点:
- ✅ 每个线程有本地队列(Thread-Local Queue)
- ✅ 工作盗取算法(Work Stealing)
- ✅ 无锁或低锁操持(Lock-Free / Low-Lock)
- ✅ 负载均衡:自动均衡线程间的使命
- ✅ 性能提升:相比 .NET Framework 提升 4-10 倍
架构图:
flowchart TD A[用户代码] --> B[ThreadPool.QueueUserWorkItem] B --> C{当火线程是
ThreadPool 线程?} C -->|是
比方: Task 内部再提交 Task| D[放入本地队列
LIFO 无锁] C -->|否
比方: 主线程/UI 线程提交| E[放入全局队列
低锁] D --> F[线程 1
本地队列] E --> G[全局队列
ConcurrentQueue] G --> F G --> H[线程 2
本地队列] G --> I[线程 3
本地队列] F -->|实行使命| J[工作 1] H -->|实行使命| K[工作 2] I -->|实行使命| L[工作 3] F -.盗取.-> H F -.盗取.-> I H -.盗取.-> F H -.盗取.-> I I -.盗取.-> F I -.盗取.-> H style D fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style E fill:#fff9c4,color:#f57f17,stroke:#f9a825,stroke-width:2px style F fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px style H fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px style I fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px Note1[✅ 本地队列无锁,性能极高
✅ 工作盗取实现负载均衡]判定逻辑详解:- // ✅ 场景 1:主线程提交 → 全局队列
- static void Main()
- {
- // 当前线程:主线程(非 ThreadPool 线程)
- ThreadPool.QueueUserWorkItem(_ =>
- {
- Console.WriteLine("任务 A");
- });
- // ↑ 任务 A 进入全局队列
- }
- // ✅ 场景 2:ThreadPool 线程内部提交 → 本地队列
- static void Main()
- {
- ThreadPool.QueueUserWorkItem(_ =>
- {
- // 当前线程:ThreadPool 线程
- Console.WriteLine("任务 A");
- ThreadPool.QueueUserWorkItem(_ =>
- {
- Console.WriteLine("任务 B");
- });
- // ↑ 任务 B 进入当前线程的本地队列(LIFO 无锁)
- });
- }
- // ✅ 场景 3:Task 内部提交 Task → 本地队列
- static async Task ProcessAsync()
- {
- await Task.Run(() =>
- {
- // 当前线程:ThreadPool 线程
- Console.WriteLine("任务 A");
- Task.Run(() =>
- {
- Console.WriteLine("任务 B");
- });
- // ↑ 任务 B 进入当前线程的本地队列
- });
- }
- // ✅ 场景 4:UI 线程提交 → 全局队列
- private void Button_Click(object sender, EventArgs e)
- {
- // 当前线程:UI 线程(非 ThreadPool 线程)
- Task.Run(() =>
- {
- Console.WriteLine("后台任务");
- });
- // ↑ 任务进入全局队列
- }
复制代码 底层实现(简化):- // .NET Core ThreadPool 内部逻辑
- public void QueueUserWorkItem(WaitCallback callback)
- {
- // 检查当前线程是否是 ThreadPool 线程
- var currentThreadLocalQueue = t_queue; // [ThreadStatic] 线程本地变量
- if (currentThreadLocalQueue != null)
- {
- // 当前是 ThreadPool 线程 → 放入本地队列
- currentThreadLocalQueue.LocalPush(callback); // LIFO 无锁
- }
- else
- {
- // 当前不是 ThreadPool 线程 → 放入全局队列
- _globalQueue.Enqueue(callback); // FIFO 低锁
- }
- }
复制代码 性能影响:
提交场景目标队列锁开销缓存友爱性性能主线程 → ThreadPool全局队列低锁(ConcurrentQueue)一样寻常中ThreadPool → ThreadPool本地队列无锁✅ 极好✅ 极高UI 线程 → ThreadPool全局队列低锁一样寻常中关键优化:
- 📌 本地队列上风:
- 无锁操持,零竞争
- 缓存友爱(刚 Push 的数据大概还在 CPU 缓存中)
- LIFO 战略进一步提升缓存掷中率
- 📌 全局队列特点:
- 利用 ConcurrentQueue(低锁,非无锁)
- 多个非 ThreadPool 线程大概同时提交(须要同步)
- 作为"托底"机制,确保全部使命都能入队
2.3 工作盗取算法(Work Stealing)详解
2.3.1 工作盗取办理的题目
题目场景:
flowchart LR A[线程 1] -->|10 个使命| B[繁忙
CPU 100%] C[线程 2] -->|0 个使命| D[空闲
CPU 0%] E[线程 3] -->|8 个使命| F[繁忙
CPU 90%] style B fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px style D fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style F fill:#ffe0b2,color:#e65100,stroke:#ef6c00,stroke-width:2px Note1[❌ 负载不均衡
❌ CPU 利用率低
❌ 使命完成慢]传统办理方案(.NET Framework):- 线程 2(空闲)→ 等待全局队列有新任务
- ↓
- 问题:
- - 如果没有新任务提交,线程 2 一直空闲
- - 线程 1 的任务完不成,整体吞吐量下降
复制代码 工作盗取办理方案(.NET Core):- 线程 2(空闲)→ 主动从线程 1 的队列中"偷"任务
- ↓
- 优点:
- - 自动负载均衡
- - 提高 CPU 利用率
- - 加速任务完成
复制代码 2.3.2 工作盗取的完备流程
sequenceDiagram participant T1 as 线程 1
(繁忙) participant Q1 as 本地队列 1
[A,B,C,D,E] participant T2 as 线程 2
(空闲) participant Q2 as 本地队列 2
[ ] participant G as 全局队列 Note over T1,Q1: 线程 1 正在实行使命 T1->>Q1: 从尾部取出使命 E
(LIFO) Q1-->>T1: 返回使命 E Note over T2,Q2: 线程 2 完成使命,本地队列为空 T2->>Q2: 实行从本地队列取使命 Q2-->>T2: ❌ 队列为空 Note over T2,G: 步调 2:实行从全局队列取使命 T2->>G: 实行从全局队列取使命 G-->>T2: ❌ 全局队列也为空 Note over T2,Q1: 步调 3:工作盗取触发! T2->>Q1: 重新部偷取使命 A
(FIFO) Q1-->>T2: ✅ 返回使命 A Note over T1,T2: 现在两个线程都在工作 T1->>T1: 实行使命 E T2->>T2: 实行使命 A Note over Q1: 本地队列 1 剩余:[B,C,D]2.3.3 本地队列的双端操持
为什么本线程用 LIFO,盗取用 FIFO?
flowchart TD A[本地队列:双端队列 Deque] --> B[尾部
LIFO
本线程访问] A --> C[头部
FIFO
其他线程盗取] B --> D[迩来到场的使命
缓存热] C --> E[最早到场的使命
缓存冷] D --> F[✅ 优点 1:缓存友爱
本线程刚 Push 的数据
大概还在 CPU 缓存中] E --> G[✅ 优点 2:镌汰竞争
本线程和盗取线程
访问差别端] style B fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style C fill:#fff9c4,color:#f57f17,stroke:#f9a825,stroke-width:2px style F fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px style G fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px代码层面的体现:2.3.4 工作盗取的实际场景演示
场景:并行处置处罚 100 个使命
sequenceDiagram participant U as 用户代码 participant TP as ThreadPool participant T1 as 线程 1 participant T2 as 线程 2 participant T3 as 线程 3 participant Q1 as 队列 1 participant Q2 as 队列 2 participant Q3 as 队列 3 Note over U,TP: 提交 100 个使命 U->>TP: 提交使命 1-100 TP->>Q1: 使命 1-33 TP->>Q2: 使命 34-66 TP->>Q3: 使命 67-100 Note over T1,Q1: 线程 1:33 个使命 Note over T2,Q2: 线程 2:33 个使命 Note over T3,Q3: 线程 3:34 个使命 par 并行实行 T1->>Q1: 实行使命 1-10 T2->>Q2: 实行使命 34-43 T3->>Q3: 实行使命 67-76 end Note over T2,Q2: 线程 2 完成得快,队列空了 T2->>Q2: 实行取使命 Q2-->>T2: ❌ 队列为空 Note over T2,Q3: 工作盗取:从线程 3 偷取 T2->>Q3: 盗取使命 77-85
(偷一半) Q3-->>T2: ✅ 返回使命 par 继续并行实行 T1->>Q1: 实行使命 11-33 T2->>T2: 实行使命 77-85 T3->>Q3: 实行使命 86-100 end Note over T1,T3: 全部使命完成
负载自动均衡2.3.5 工作盗取的性能上风
性能对比实行:
场景.NET Framework 4.8.NET Core / .NET 5+性能提升少量使命(10 个)5 ms4 ms约 1.25 倍中等使命(1000 个)200 ms50 ms约 4 倍大量使命(100000 个)10000 ms1000 ms约 10 倍不均衡使命15000 ms2000 ms约 7.5 倍不均衡使命场景:- // 模拟不均衡任务:某些任务耗时长,某些任务耗时短
- for (int i = 0; i < 1000; i++)
- {
- int taskId = i;
- ThreadPool.QueueUserWorkItem(_ =>
- {
- // 随机耗时:10-1000ms
- Thread.Sleep(Random.Shared.Next(10, 1000));
- });
- }
- // .NET Framework:
- // - 某些线程可能一直执行短任务(快速完成)
- // - 某些线程可能一直执行长任务(阻塞很久)
- // - 无法自动平衡
- // .NET Core(工作窃取):
- // - 完成短任务的线程会自动"偷"长任务线程的任务
- // - 自动负载均衡
- // - 整体完成时间大幅缩短
复制代码 2.3.6 工作盗取的代码示例
完备的模仿实现(简化版):2.4 ThreadPool 的使命调治流程
2.4.1 使命提交的完备流程
flowchart TD A[用户代码] --> B{在 ThreadPool 线程中?} B -->|是| C[放入当火线程的
本地队列
LIFO 无锁] B -->|否| D[放入全局队列
ConcurrentQueue] C --> E[本地队列] D --> F[全局队列] E --> G[线程 1] E --> H[线程 2
可以盗取] E --> I[线程 3
可以盗取] F --> G F --> H F --> I G --> J[实行使命] H --> K[实行使命] I --> L[实行使命] J --> M{本地队列空?} K --> N{本地队列空?} L --> O{本地队列空?} M -->|是| P[实行全局队列] N -->|是| P O -->|是| P P --> Q{全局队列空?} Q -->|是| R[工作盗取] Q -->|否| F R --> E style C fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style D fill:#fff9c4,color:#f57f17,stroke:#f9a825,stroke-width:2px style R fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px2.4.2 线程调治的优先级
线程获取使命的优先级次序:
flowchart LR A[线程空闲] --> B{1.本地队列有使命?} B -->|是| C[✅ 从本地队列取
LIFO 无锁
最快] B -->|否| D{2.全局队列有使命?} D -->|是| E[✅ 从全局队列取
FIFO 低锁
较快] D -->|否| F{3.其他线程有使命?} F -->|是| G[✅ 工作盗取
FIFO 低锁
慢] F -->|否| H[❌ 线程休眠
等待新使命] C --> I[实行使命] E --> I G --> I I --> A H --> J[新使命到来] J --> A style C fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style E fill:#fff9c4,color:#f57f17,stroke:#f9a825,stroke-width:2px style G fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px style H fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px优先级总结:
- 最高优先级:本地队列(LIFO,无锁,缓存友爱)
- 次优先级:全局队列(FIFO,低锁)
- 兜底战略:工作盗取(FIFO,从其他线程偷取)
- 无使命时:线程休眠,等待新使命叫醒
2.5 ThreadPool 的动态调解
2.5.1 MinThreads 和 MaxThreads
- // 查看当前配置
- ThreadPool.GetMinThreads(out int minWorker, out int minIO);
- ThreadPool.GetMaxThreads(out int maxWorker, out int maxIO);
- Console.WriteLine($"最小工作线程:{minWorker}");
- Console.WriteLine($"最大工作线程:{maxWorker}");
- Console.WriteLine($"最小 I/O 线程:{minIO}");
- Console.WriteLine($"最大 I/O 线程:{maxIO}");
- // 典型输出(8 核 CPU):
- // 最小工作线程:8(通常 = CPU 核心数)
- // 最大工作线程:32767
- // 最小 I/O 线程:8
- // 最大 I/O 线程:1000
复制代码 设置阐明:
参数默认值阐明MinThreadsCPU 焦点数线程池启动时立刻创建的线程数MaxThreads32767(工作线程)线程池可以创建的最大线程数I/O Threads单独的 I/O 完成端口线程用于处置处罚异步 I/O 完成2.5.2 线程注入战略(Hill Climbing 算法)
场景:使命突然增长
sequenceDiagram participant U as 用户代码 participant TP as ThreadPool participant HC as Hill Climbing
算法 participant OS as 利用体系 Note over U,TP: T0: 初始状态 U->>TP: 提交 100 个使命 TP->>TP: 当火线程:8 个 (MinThreads) TP->>TP: 全部线程都繁忙 Note over TP,HC: T1: 500ms 后 TP->>HC: 检测:全部线程繁忙
使命队列积存 HC->>HC: 盘算吞吐量:100 tasks/s HC->>TP: 决定:注入 1 个新线程 TP->>OS: 创建新线程 9 Note over TP,HC: T2: 1000ms 后(再等 500ms) TP->>HC: 检测:仍然繁忙 HC->>HC: 盘算吞吐量:120 tasks/s ↑ HC->>TP: 决定:注入 1 个新线程 TP->>OS: 创建新线程 10 Note over TP,HC: T3: 1500ms 后 TP->>HC: 检测:仍然繁忙 HC->>HC: 盘算吞吐量:150 tasks/s ↑ HC->>TP: 决定:注入 2 个新线程
(加速注入) TP->>OS: 创建新线程 11, 12 Note over TP,HC: T4: 2000ms 后 TP->>HC: 检测:仍然繁忙 HC->>HC: 盘算吞吐量:140 tasks/s ↓ HC->>TP: 决定:克制注入
(吞吐量降落) Note over TP: 线程数稳固在 12 个Hill Climbing 算法原理:
flowchart TD A[检测线程池状态] --> B{全部线程繁忙?} B -->|否| C[保持当火线程数] B -->|是| D[等待 500ms
观察吞吐量] D --> E[丈量吞吐量
Throughput] E --> F{吞吐量厘革?} F -->|上升 ↑| G[✅ 注入新线程
继续爬坡] F -->|降落 ↓| H[❌ 克制注入
线程数已达最优] F -->|持平 →| I[⚠️ 小幅调解
继续观察] G --> J[创建 1-2 个新线程] J --> A H --> C I --> A C --> K[连续监控 ] K --> A style G fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style H fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px style I fill:#fff9c4,color:#f57f17,stroke:#f9a825,stroke-width:2pxHill Climbing 焦点逻辑:
⚠️ 注意:上面流程图中的时间隔断(500ms)是简化值,便于明白。实际的检测隔断是动态的,从初始的 15-30ms 徐徐增长到数百毫秒乃至数秒,详细取决于体系负载和吞吐量厘革。
- 🎯 目标:找到最优线程数目,使吞吐量最大化
- 📈 战略:徐徐增长线程,监控
吞吐量厘革
- 📉 决定:
- 吞吐量上升 → 继续增长线程
- 吞吐量降落 → 克制增长(过多线程导致上下文切换)
- 吞吐量持平 → 小幅调解,继续观察
为什么不立刻创建大量线程?- 假设立即创建 100 个线程:
- ↓
- ❌ 内存占用:100 MB
- ❌ 上下文切换:频繁
- ❌ CPU 缓存失效:严重
- ❌ 吞吐量下降:可能比 10 个线程还慢
复制代码 Hill Climbing 的智能之处:- 逐步增加线程:
- ↓
- ✅ 自动找到最优点
- ✅ 避免过度创建
- ✅ 适应不同工作负载
- ✅ 动态调整(任务减少时会减少线程)
复制代码 2.5.3 线程接纳战略
空闲线程的生命周期:
stateDiagram-v2
--> Working: 实行使命 Working --> Idle: 使命完成 Idle --> Working: 有新使命 Idle --> Waiting: 等待 20 秒 Waiting --> Working: 有新使命 Waiting --> Terminated: 超时仍无使命
且线程数 > MinThreads Terminated -->
note right of Idle 线程空闲,等待新使命 end note note right of Waiting 凌驾 20 秒无使命 预备克制 end note note right of Terminated 线程烧毁 开释资源 end note线程接纳据件:- // 伪代码:ThreadPool 内部逻辑
- while (true)
- {
- if (TryGetWork(out var workItem))
- {
- // 有任务,执行
- workItem.Execute();
- }
- else
- {
- // 无任务,空闲
- if (WaitForWork(timeout: 20_000)) // 等待 20 秒
- {
- // 有新任务到来,继续工作
- continue;
- }
- else
- {
- // 20 秒后仍无任务
- if (ThreadPool.ThreadCount > ThreadPool.MinThreads)
- {
- // 线程数超过最小值,可以退出
- Console.WriteLine($"线程 {Environment.CurrentManagedThreadId} 退出");
- return; // 线程终止
- }
- else
- {
- // 线程数等于最小值,不能退出,继续等待
- continue;
- }
- }
- }
- }
复制代码 2.5.4 实行:观察线程池动态调解
- // 代码示例:ThreadPoolDynamicDemo.cs
- static void DemonstrateThreadPoolDynamic()
- {
- ThreadPool.SetMinThreads(2, 2); // 设置最小线程数为 2
- Console.WriteLine("开始提交任务...");
- for (int i = 0; i < 50; i++)
- {
- int taskId = i;
- ThreadPool.QueueUserWorkItem(_ =>
- {
- Console.WriteLine($"[任务 {taskId}] 线程 {Environment.CurrentManagedThreadId} 开始");
- Thread.Sleep(2000); // 模拟工作
- Console.WriteLine($"[任务 {taskId}] 完成");
- });
- if (i % 10 == 0)
- {
- ThreadPool.GetAvailableThreads(out int worker, out int io);
- Console.WriteLine($"--- 已提交 {i} 个任务,当前线程池线程数:{ThreadPool.ThreadCount},可用:{worker} ---");
- }
- Thread.Sleep(100); // 稍微延迟,观察线程注入
- }
- Console.WriteLine("\n等待所有任务完成...");
- Thread.Sleep(10000);
- ThreadPool.GetAvailableThreads(out int finalWorker, out int finalIo);
- Console.WriteLine($"\n最终线程池状态:");
- Console.WriteLine($" 线程数:{ThreadPool.ThreadCount}");
- Console.WriteLine($" 可用工作线程:{finalWorker}");
- }
- // 预期输出:
- // --- 已提交 0 个任务,当前线程池线程数:2,可用:... ---
- // [任务 0] 线程 4 开始
- // [任务 1] 线程 5 开始
- // (等待 500ms,Hill Climbing 决策)
- // --- 已提交 10 个任务,当前线程池线程数:3,可用:... ---
- // [任务 2] 线程 6 开始
- // (等待 500ms)
- // --- 已提交 20 个任务,当前线程池线程数:4,可用:... ---
- // ...
- // (逐步增加线程数)
- // --- 已提交 40 个任务,当前线程池线程数:8,可用:... ---
复制代码 观察要点:
- 初始阶段:只有 2 个线程(MinThreads)
- 使命积存:全部线程繁忙,使命列队
- 徐徐注入:每 500ms 注入 1-2 个新线程
- 到达稳固:吞吐量不再提升,克制注入
- 使命完成后:空闲线程在 20 秒后徐徐退出
2.6 ThreadPool 的性能监控
2.6.1 及时监控 API
- // 获取线程池状态
- ThreadPool.GetAvailableThreads(out int availableWorker, out int availableIO);
- ThreadPool.GetMinThreads(out int minWorker, out int minIO);
- ThreadPool.GetMaxThreads(out int maxWorker, out int maxIO);
- // 计算忙碌线程数
- int busyWorker = maxWorker - availableWorker;
- int busyIO = maxIO - availableIO;
- // .NET 5+ 新增:获取挂起任务数
- long pendingItems = ThreadPool.PendingWorkItemCount;
- // .NET 7+ 新增:获取完成任务数
- long completedItems = ThreadPool.CompletedWorkItemCount;
- Console.WriteLine($"工作线程:{busyWorker}/{maxWorker} 忙碌");
- Console.WriteLine($"I/O 线程:{busyIO}/{maxIO} 忙碌");
- Console.WriteLine($"挂起任务:{pendingItems}");
- Console.WriteLine($"已完成任务:{completedItems}");
复制代码 2.6.2 监控示例
- // 代码示例:ThreadPoolMonitorDemo.cs
- static void MonitorThreadPool()
- {
- var timer = new System.Timers.Timer(1000);
- timer.Elapsed += (s, e) =>
- {
- ThreadPool.GetAvailableThreads(out int worker, out int io);
- ThreadPool.GetMinThreads(out int minWorker, out int minIo);
- ThreadPool.GetMaxThreads(out int maxWorker, out int maxIo);
- int busyWorker = maxWorker - worker;
- long pending = ThreadPool.PendingWorkItemCount;
- Console.WriteLine($"[{DateTime.Now:HH:mm:ss}]");
- Console.WriteLine($" 工作线程:{busyWorker}/{maxWorker} 忙碌,{worker} 可用");
- Console.WriteLine($" I/O 线程:{maxIo - io}/{maxIo} 忙碌");
- Console.WriteLine($" 挂起任务:{pending}");
- Console.WriteLine($" 当前线程数:{ThreadPool.ThreadCount}");
- Console.WriteLine("---");
- };
- timer.Start();
- // 提交一些任务
- for (int i = 0; i < 100; i++)
- {
- ThreadPool.QueueUserWorkItem(_ => Thread.Sleep(5000));
- }
- Console.WriteLine("按 Enter 停止监控...");
- Console.ReadLine();
- timer.Stop();
- }
- // 输出示例:
- // [14:30:00]
- // 工作线程:8/32767 忙碌,32759 可用
- // I/O 线程:0/1000 忙碌
- // 挂起任务:92
- // 当前线程数:8
- // ---
- // [14:30:01](500ms 后,Hill Climbing 注入新线程)
- // 工作线程:9/32767 忙碌,32758 可用
- // I/O 线程:0/1000 忙碌
- // 挂起任务:83
- // 当前线程数:9
- // ---
复制代码 2.7 ThreadPool 的最佳实践
✅ 得当利用 ThreadPool 的场景
- // 1. 短期任务(< 1 秒)
- ThreadPool.QueueUserWorkItem(_ =>
- {
- ProcessData(); // 快速完成
- });
- // 2. CPU 密集型任务(数量可控)
- for (int i = 0; i < 100; i++)
- {
- ThreadPool.QueueUserWorkItem(_ => ComputePrimes());
- }
- // 3. 并行计算
- Parallel.For(0, 1000, i =>
- {
- // Parallel 内部使用 ThreadPool
- ProcessItem(i);
- });
复制代码 ❌ 不得当利用 ThreadPool 的场景
- // 1. 长期运行任务(会耗尽线程池)
- ThreadPool.QueueUserWorkItem(_ =>
- {
- while (true) // ❌ 永远运行,占用线程池线程
- {
- Monitor();
- Thread.Sleep(1000);
- }
- });
- // ✅ 正确做法:使用专用后台线程
- var monitorThread = new Thread(() =>
- {
- while (true)
- {
- Monitor();
- Thread.Sleep(1000);
- }
- })
- {
- IsBackground = true
- };
- monitorThread.Start();
- // 2. I/O 密集型任务(应使用 async/await)
- ThreadPool.QueueUserWorkItem(_ =>
- {
- var data = httpClient.GetStringAsync(url).Result; // ❌ 阻塞线程
- });
- // ✅ 正确做法:使用异步
- await httpClient.GetStringAsync(url); // 不阻塞线程
- // 3. 需要精确控制的任务
- ThreadPool.QueueUserWorkItem(_ =>
- {
- // ❌ 无法设置线程优先级
- // ❌ 无法设置线程名称
- // ❌ 无法控制线程生命周期
- });
- // ✅ 正确做法:使用专用线程
- var thread = new Thread(() => { /* work */ })
- {
- Priority = ThreadPriority.High,
- Name = "Worker-1"
- };
- thread.Start();
复制代码 2.8 性能对比:.NET Framework vs .NET Core
2.8.1 微基准测试
- // 代码示例:需要分别在两个项目中运行
- // 项目 1:Threads(.NET 10)
- // 项目 2:ThreadsFramework(.NET Framework 4.8)
- static void BenchmarkThreadPool()
- {
- const int taskCount = 100_000;
- var sw = Stopwatch.StartNew();
- var countdown = new CountdownEvent(taskCount);
- for (int i = 0; i < taskCount; i++)
- {
- ThreadPool.QueueUserWorkItem(_ =>
- {
- // 简单工作
- var result = Math.Sqrt(42);
- countdown.Signal();
- });
- }
- countdown.Wait();
- sw.Stop();
- Console.WriteLine($"完成 {taskCount:N0} 个任务:{sw.ElapsedMilliseconds} ms");
- Console.WriteLine($"吞吐量:{taskCount * 1000.0 / sw.ElapsedMilliseconds:N0} tasks/s");
- }
- // 实际测试结果(8 核 CPU):
- // .NET Framework 4.8:
- // 完成 100,000 个任务:2,000 ms
- // 吞吐量:50,000 tasks/s
- //
- // .NET Core / .NET 10:
- // 完成 100,000 个任务:500 ms
- // 吞吐量:200,000 tasks/s
- //
- // 性能提升:4 倍!
复制代码 2.8.2 性能对比表
场景.NET Framework 4.8.NET Core / .NET 5+性能提升缘故原由少量使命(100)10 ms8 ms1.25x锁竞争镌汰中等使命(10,000)200 ms50 ms4x工作盗取大量使命(100,000)2,000 ms500 ms4x无锁队列不均衡使命3,000 ms600 ms5x负载均衡高并发提交1,500 ms300 ms5x本地队列小结:
ThreadPool 通过线程复用、工作盗取算法和智能调解,大幅提升了并发性能。.NET Core 的重构使其成为当代并发编程的基石。工作盗取机制办理了负载不均衡题目,使得线程池可以大概自动顺应各种工作负载,实现最优的 CPU 利用率。
📦 Part 3: Task 的本质(重点)
3.1 Task 是什么?
焦点观点:Task 不是线程,Task 是异步利用的抽象。- Task = Promise/Future 模式的实现
- Promise:承诺将来会有一个结果
- Future:未来的结果
- Task:
- - 可能已经完成
- - 可能正在执行
- - 可能还未开始
- - 可能失败(异常)
- - 可能被取消
复制代码 Task 的状态机
可视化状态转换:
stateDiagram-v2
--> Created: new Task(...) Created --> WaitingForActivation: task.Start() Created --> WaitingToRun: Task.Run(...) WaitingForActivation --> WaitingToRun: 进入调治队列 WaitingToRun --> Running: 线程开始实行 Running --> RanToCompletion: ✅ 正常完成 Running --> Faulted: ❌ 抛出未处置处罚非常 Running --> Canceled: ⚠️ CancellationToken 取消 RanToCompletion -->
Faulted -->
Canceled -->
note right of Created Task 对象已创建 但尚未启动 end note note right of WaitingToRun 已到场 ThreadPool 队列 等待线程实行 end note note right of Running 正在某个线程上实行 task.IsCompleted = false end note note right of RanToCompletion 乐成完成 task.IsCompletedSuccessfully = true end note note right of Faulted 非常状态 task.IsFaulted = true task.Exception 包罗非常信息 end note note right of Canceled 已取消 task.IsCanceled = true end note状态属性总结:
状态IsCompletedIsCompletedSuccessfullyIsFaultedIsCanceledCreatedfalsefalsefalsefalseWaitingToRunfalsefalsefalsefalseRunningfalsefalsefalsefalseRanToCompletion✅ true✅ truefalsefalseFaulted✅ truefalse✅ truefalseCanceled✅ truefalsefalse✅ true代码示例:- // 代码示例:TaskStateDemo.cs
- static void DemonstrateTaskStates()
- {
- // Created
- var task = new Task(() => Thread.Sleep(1000));
- Console.WriteLine($"状态:{task.Status}"); // Created
- // WaitingForActivation
- task.Start();
- Console.WriteLine($"状态:{task.Status}"); // WaitingToRun / Running
- // Running
- Thread.Sleep(100);
- Console.WriteLine($"状态:{task.Status}"); // Running
- // RanToCompletion
- task.Wait();
- Console.WriteLine($"状态:{task.Status}"); // RanToCompletion
- }
复制代码 3.2 Task 与 ThreadPool 的协作
Task.Run 的本质
- // 源码简化版(CoreCLR)
- public static Task Run(Action action)
- {
- return Task.InternalStartNew(
- null,
- action,
- null,
- CancellationToken.None,
- TaskScheduler.Default, // ← 关键:使用 ThreadPoolTaskScheduler
- TaskCreationOptions.DenyChildAttach,
- InternalTaskOptions.None
- );
- }
复制代码 完备流程可视化:
flowchart TD A[用户代码调用
Task.Run] --> B[Task.Run 方法] B --> C[调用 Task.InternalStartNew] C --> D[在托管堆分配 Task 对象
约 200 bytes] D --> E[设置 Task 属性
- _action = lambda
- _scheduler = Default
- _state = WaitingToRun] E --> F[TaskScheduler.Default
ThreadPoolTaskScheduler] F --> G[调用 QueueTask] G --> H{当火线程是
ThreadPool 线程?} H -->|是| I[放入本地队列
LIFO 无锁
性能最优] H -->|否| J[放入全局队列
ConcurrentQueue
低锁操持] I --> K[ThreadPool
工作队列体系] J --> K K --> L{线程池有
空闲线程?} L -->|是| M[从线程池取出线程
复用现有线程
无创建开销] L -->|否| N[Hill Climbing 算法
评估是否须要新线程] N --> O{到达
MaxThreads?} O -->|否| P[创建新线程
OS 调用
约 1 MB 内存] O -->|是| Q[使命列队等待
直到有线程可用] M --> R[线程实行
task.Execute] P --> R Q --> R R --> S[实行用户 lambda
action.Invoke] S --> T{实行效果?} T -->|乐成| U[Task.Status =
RanToCompletion] T -->|非常| V[Task.Status =
Faulted
生存非常到 Task.Exception] T -->|取消| W[Task.Status =
Canceled] U --> X[触发 continuation
实行 await 后续代码] V --> X W --> X X --> Y[线程归还线程池
等待复用
不烧毁] style A fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px style D fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style I fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style J fill:#fff9c4,color:#f57f17,stroke:#f9a825,stroke-width:2px style M fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style P fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px style Q fill:#ffe0b2,color:#e65100,stroke:#ef6c00,stroke-width:2px style Y fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px关键流程阐明:
阶段利用资源阐明1. 对象创建分配 Task 对象约 200 bytes托管堆分配,GC 管理2. 调治决定选择队列(本地/全局)< 1 微秒无锁本地队列最快3. 线程获取复用或创建线程0 或 1 MB优先复用,克制创建4. 实行使命运行用户代码取决于使命真正的工作负载5. 状态更新设置完成状态< 1 微秒触发 continuation6. 线程接纳归还线程池0线程复用,无烧毁资源性能优化点:
- ✅ 本地队列优先:在 ThreadPool 线程内提交的使命利用 LIFO 无锁本地队列,性能最优
- ✅ 线程复用:优先利用线程池现有线程,克制昂贵的线程创建(1 MB 内存 + OS 调用)
- ✅ 智能扩展:Hill Climbing 算法动态决定是否创建新线程,均衡吞吐量和资源斲丧
- ✅ 零烧毁资源:线程完成使命后归还线程池,不烧毁,等待下次复用
Task 的内存开销
- // Task 对象的大致结构(简化)
- class Task
- {
- private int _stateFlags; // 4 bytes
- private object _action; // 8 bytes(64 位)
- private object _result; // 8 bytes
- private TaskScheduler _scheduler; // 8 bytes
- private volatile int _id; // 4 bytes
- private CancellationToken _token; // ~16 bytes
- // ... 其他字段
-
- // 总计:约 100-200 bytes
- }
复制代码 对比:- 1 个 Thread:约 1 MB
- 1 个 Task:约 100-200 bytes
- 10000 个 Thread:约 10 GB
- 10000 个 Task:约 1-2 MB(内存节省 5000 倍!)
复制代码 3.3 I/O 麋集型 Task 不占用线程
焦点区别:- // CPU 密集型 Task(占用线程)
- var task1 = Task.Run(() =>
- {
- for (int i = 0; i < 1000000; i++)
- {
- var result = Math.Sqrt(i); // CPU 计算
- }
- });
- // I/O 密集型 Task(不占用线程)
- var task2 = Task.Run(async () =>
- {
- await Task.Delay(1000); // ← 这里不占用线程!
- });
复制代码 底层机制可视化:
sequenceDiagram participant User as 用户代码 participant Task as Task 对象 participant Pool as ThreadPool participant Thread as 工作线程 participant OS as 利用体系
定时器/IOCP participant IOThread as I/O 完成
线程 Note over User,Thread: 第一阶段:启动异步利用 User->>Task: await Task.Delay(1000) Task->> ool: 哀求工作线程实行 Pool->>Thread: 分配线程 Thread->>Thread: 实行到 await 表达式 Note over Thread,OS: 第二阶段:发起 I/O 哀求,开释线程 Thread->>OS: 创建 OS 定时器
Windows: CreateThreadpoolTimer
Linux: timerfd_create OS-->>Thread: 返回定时器句柄 Thread->>Task: 设置 continuation
生存后续代码 Thread->> ool: 线程归还线程池
状态:可用 Note over Thread: 线程被开释!
可以处置处罚其他使命 Note over OS: 第三阶段:等待期(1000ms)
无线程占用! OS->>OS: 定时器计时中...
1000ms 倒计时 Note over User,IOThread: 1000ms 期间:
❌ 没有线程被占用
✅ 线程可以处置处罚其他工作
✅ 内存开销:仅 Task 对象(约 200 bytes) Note over OS,IOThread: 第四阶段:I/O 完成关照 OS->>OS: 定时器到期! OS->>IOThread: 触发 I/O 完成关照
Windows: IOCP
Linux: epoll IOThread->>Task: 处置处罚完成回调
Task.Status = RanToCompletion Note over IOThread,Pool: 第五阶段:规复实行 IOThread->> ool: 哀求线程实行 continuation Pool->>Thread: 分配线程
大概是差别的线程! Thread->>User: 实行 await 后续代码 Note over User: await 后的代码继续实行 rect rgb(200, 230, 201) Note over Thread,OS: 关键洞察:
线程在等待期间完全空闲
1000ms 内线程数不会增长 end rect rgb(255, 224, 178) Note over OS,IOThread: I/O 完成端口(IOCP)
专门的 I/O 线程池处置处罚回调
与工作线程池分离 endCPU 麋集型 vs I/O 麋集型对比:
sequenceDiagram participant User1 as CPU 使命 participant Thread1 as 工作线程 1 participant User2 as I/O 使命 participant Thread2 as 工作线程 2 participant OS as 利用体系 Note over User1,Thread1: CPU 麋集型:线程连续占用 User1->>Thread1: Task.Run(() => Compute()) activate Thread1 Thread1->>Thread1: for i=0 to 1000000 Note over Thread1: 线程连续工作
占用 CPU Thread1->>Thread1: Math.Sqrt(i) Note over Thread1: 无法开释
不停到盘算完成 Thread1->>Thread1: 继续盘算... Thread1-->>User1: 盘算完成 deactivate Thread1 Note over User2,OS: I/O 麋集型:线程快速开释 User2->>Thread2: await Task.Delay(1000) activate Thread2 Thread2->>OS: 创建定时器 OS-->>Thread2: 定时器已创建 Thread2->>Thread2: 生存 continuation Thread2-->>User2: 线程立刻开释 deactivate Thread2 Note over Thread2: ✅ 线程空闲
可处置处罚其他使命 Note over OS: 定时器计时中...
1000ms OS->>OS: 定时器到期 OS->>User2: 触发回调 User2->>Thread2: 哀求新线程 activate Thread2 Thread2->>User2: 实行后续代码 deactivate Thread2 rect rgb(255, 205, 210) Note over Thread1: CPU 使命:
线程占用时间 = 盘算时间
❌ 无法复用 end rect rgb(200, 230, 201) Note over Thread2,OS: I/O 使命:
线程占用时间 ≈ 0
✅ 等待期间可复用 end关键差别总结:
特性CPU 麋集型(如盘算)I/O 麋集型(如网络、文件、延伸)线程占用✅ 连续占用❌ 等待期间不占用等待机制CPU 实行循环OS 级别的异步机制可扩展性受限于线程池巨细✅ 险些无穷(仅受内存限定)10000 个使命须要数百个线程✅ 只需 10-20 个线程性能瓶颈CPU 焦点数网络带宽/磁盘 I/O实用 APITask.Run(() => Compute())await httpClient.GetAsync()底层技能栈:
平台异步 I/O 机制阐明WindowsI/O Completion Ports (IOCP)高效的异步 I/O 完成关照Linuxepoll / io_uring高性能事故关照机制macOSkqueueBSD 体系的事故关照内存对比:- 10000 个 CPU 密集型 Task:
- - 需要约 100 个线程(假设长时间运行)
- - 内存:100 × 1 MB = 100 MB(线程栈)
- - 任务对象:10000 × 200 bytes = 2 MB
- - 总计:约 102 MB
- 10000 个 I/O 密集型 Task:
- - 需要约 10-20 个线程(快速释放)
- - 内存:20 × 1 MB = 20 MB(线程栈)
- - 任务对象:10000 × 200 bytes = 2 MB
- - 总计:约 22 MB
- 内存节省:80 MB(约 78%)
复制代码 实行:验证 I/O 利用不占用线程
- // 代码示例:IoTaskThreadDemo.cs
- static async Task DemonstrateIoTaskThreadUsage()
- {
- Console.WriteLine($"初始线程数:{ThreadPool.ThreadCount}");
- // 创建 10000 个 I/O 密集型 Task
- var tasks = new Task[10000];
- for (int i = 0; i < 10000; i++)
- {
- tasks[i] = Task.Run(async () =>
- {
- await Task.Delay(5000); // I/O 操作
- });
- }
- await Task.Delay(100); // 等待任务启动
- Console.WriteLine($"10000 个 I/O Task 运行中,线程数:{ThreadPool.ThreadCount}");
- // 预期输出:约 10-20 个(而不是 10000 个!)
- await Task.WhenAll(tasks);
- }
复制代码 3.4 Task 的调治器(TaskScheduler)
TaskScheduler 架构条理:
flowchart TD A[Task API
Task.Run / Task.Factory.StartNew] --> B{利用哪个
TaskScheduler?} B -->|默认
95% 场景| C[TaskScheduler.Default
ThreadPoolTaskScheduler] B -->|UI 线程| D[TaskScheduler.FromCurrentSynchronizationContext
SynchronizationContextTaskScheduler] B -->|自界说| E[自界说 TaskScheduler
继续 TaskScheduler 抽象类] C --> F[ThreadPool 队列体系
工作盗取 + 全局队列] F --> G[工作线程池实行
多核并发] D --> H[UI 消息循环
单线程实行] H --> I[UI 线程
WPF/WinForms Dispatcher] E --> J{自界说调治逻辑} J -->|限流| K[LimitedConcurrencyLevelTaskScheduler
最多 N 个并发] J -->|优先级| L[PriorityTaskScheduler
按优先级调治] J -->|线程亲和性| M[ThreadAffinityTaskScheduler
绑定特定线程] K --> N[内队伍列 + 信号量
控制并发数] L --> O[优先级队列
高优先级先实行] M --> P[专用线程池
线程本地化] style C fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style D fill:#fff9c4,color:#f57f17,stroke:#f9a825,stroke-width:2px style E fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px style K fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px style L fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px style M fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px调治器范例详解:
调治器范例利用场景特点示例代码ThreadPoolTaskScheduler默认,通用背景使命高吞吐量,工作盗取Task.Run(() => Work())SynchronizationContextTaskSchedulerUI 线程(WPF/WinForms)单线程,次序实行Task.Run(() => Work()).ContinueWith(_ => UpdateUI(), TaskScheduler.FromCurrentSynchronizationContext())LimitedConcurrencyLevelTaskScheduler限流场景(如限定数据库毗连)控制最大并发数new LimitedConcurrencyLevelTaskScheduler(4)CurrentThreadTaskScheduler同步实行(测试)在当火线程立刻实行TaskScheduler.Current默认调治器
- // TaskScheduler.Default = ThreadPoolTaskScheduler
- Task.Run(() => Console.WriteLine("使用默认调度器"));
- // 等价于
- Task.Factory.StartNew(
- () => Console.WriteLine("使用默认调度器"),
- CancellationToken.None,
- TaskCreationOptions.None,
- TaskScheduler.Default // ← ThreadPoolTaskScheduler
- );
复制代码 调治流程可视化:
sequenceDiagram participant User as 用户代码 participant Task as Task 对象 participant Scheduler as TaskScheduler participant Pool as ThreadPool participant Thread as 工作线程 User->>Task: Task.Factory.StartNew(action, scheduler) Task->>Task: 创建 Task 对象
生存 action 和 scheduler Task->>Scheduler: scheduler.QueueTask(task) alt Default Scheduler (ThreadPoolTaskScheduler) Scheduler->> ool: 调用 ThreadPool.QueueUserWorkItem Pool->>Thread: 分配线程实行 Thread->>Task: task.Execute() Task-->>User: 返回效果 else UI Scheduler (SynchronizationContextTaskScheduler) Scheduler->>Scheduler: SynchronizationContext.Post Scheduler->>Thread: 在 UI 线程实行 Thread->>Task: task.Execute() Task-->>User: 返回效果 else Custom Scheduler (自界说) Scheduler->>Scheduler: 自界说调治逻辑
如限流、优先级 Scheduler->>Thread: 按自界说规则实行 Thread->>Task: task.Execute() Task-->>User: 返回效果 end自界说调治器示例
示例 1:限定并发数的调治器- // 示例:限制并发数的调度器
- var scheduler = new LimitedConcurrencyLevelTaskScheduler(maxDegreeOfParallelism: 4);
- for (int i = 0; i < 100; i++)
- {
- Task.Factory.StartNew(
- () => { /* 工作 */ },
- CancellationToken.None,
- TaskCreationOptions.None,
- scheduler // 最多 4 个任务并发执行
- );
- }
复制代码 自界说调治器内部机制:
flowchart TD A[100 个 Task 提交] --> B[LimitedConcurrencyLevelTaskScheduler] B --> C{当前运行数
< 4?} C -->|是| D[立刻调治实行
运行数 +1] C -->|否| E[到场内队伍列
等待] D --> F[ThreadPool 实行使命] F --> G[使命完成] G --> H[运行数 -1] H --> I{内队伍列
有等待使命?} I -->|是| J[从队列取出一个使命
开始实行] I -->|否| K[调治器空闲] J --> D E -.等待.-> I style B fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px style C fill:#fff9c4,color:#f57f17,stroke:#f9a825,stroke-width:2px style D fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style E fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px示例 2:UI 线程调治器- // WPF/WinForms:确保在 UI 线程更新界面
- private async void Button_Click(object sender, EventArgs e)
- {
- // 后台任务
- var result = await Task.Run(() =>
- {
- // 在 ThreadPool 线程执行(非 UI 线程)
- return ExpensiveComputation();
- });
- // 自动回到 UI 线程
- // await 后的代码在原 SynchronizationContext 执行
- this.TextBox.Text = result; // ✅ 安全:在 UI 线程
- }
- // 或者显式指定 UI 调度器
- await Task.Run(() => Work())
- .ContinueWith(
- t => UpdateUI(t.Result),
- TaskScheduler.FromCurrentSynchronizationContext() // UI 线程
- );
复制代码 关键洞察:
- ✅ TaskScheduler 是抽象层:解耦 Task 和实行机制,提供机动性
- ✅ 默认调治器最优:95% 场景利用 ThreadPoolTaskScheduler 即可
- ✅ UI 调治器关键:确保 UI 更新在精确线程实行,克制跨线程非常
- ✅ 自界说调治器强盛:可实现限流、优先级、亲和性等高级功能
3.5 Task vs Thread vs ThreadPool 总结
特性ThreadThreadPoolTask创建资源高(OS 调用)低(复用)低(内存分配)内存占用约 1 MB共享线程约 200 bytes实用场景恒久背景使命短期 CPU 使命异步利用抽象I/O 支持❌ 壅闭❌ 壅闭✅ 不壅闭机动性高(准确控制)中高(async/await)保举利用很少少(内部利用)✅ 首选小结:
Task 是当代 .NET 并发编程的焦点抽象。它通过 ThreadPool 复用线程(CPU 麋集型)或完全不占用线程(I/O 麋集型),实现了高效的并发。
🔗 Part 4: 三者关系图解
在明白了 Thread、ThreadPool 和 Task 的独立机制后,现在让我们从宏观视角看它们是怎样协同工作的。
4.1 架构条理
从上到下的完备调用链:
flowchart TB App["用户代码 Application
━━━━━━━━━━━━━━━━━━━━
async/await
Task.Run
Parallel"] TaskAPI["Task API
━━━━━━━━━━━━━━━━━━━━
Task.Run()
Task.WhenAll()
等等"] Scheduler["TaskScheduler
━━━━━━━━━━━━━━━━━━━━
Default = ThreadPoolTaskScheduler"] ThreadPool["ThreadPool
━━━━━━━━━━━━━━━━━━━━
全局队列 + 本地队列 + 工作盗取"] Thread["Thread
━━━━━━━━━━━━━━━━━━━━
OS 线程
复用,不频仍创建/烧毁"] OS["利用体系
Windows / Linux
━━━━━━━━━━━━━━━━━━━━
线程调治器 + 上下文切换"] App --> TaskAPI TaskAPI --> Scheduler Scheduler --> ThreadPool ThreadPool --> Thread Thread --> OS style App fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px style TaskAPI fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style Scheduler fill:#fff9c4,color:#f57f17,stroke:#f9a825,stroke-width:2px style ThreadPool fill:#ffe0b2,color:#e65100,stroke:#ef6c00,stroke-width:2px style Thread fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px style OS fill:#e1bee7,color:#6a1b9a,stroke:#7b1fa2,stroke-width:2px关键明白:
- ✅ 用户代码:利用高层抽象(Task、async/await)
- ✅ Task API:提供同一的异步接口
- ✅ TaskScheduler:决定 Task 怎样调治
- ✅ ThreadPool:管理和复用线程
- ✅ Thread:OS 级别的实行单位
- ✅ 利用体系:线程调治和资源管理
4.2 Thread vs Task 实行流程对比
流程 1:new Thread() 的完备流程
flowchart TD A1[用户代码: new Thread] --> B1[创建 Thread 对象
托管内存: 约 100 bytes] B1 --> C1{调用 Start?} C1 -->|否| D1[仅托管对象
不斲丧 OS 资源] C1 -->|是| E1[P/Invoke: 调用 OS API] E1 --> F1[CreateThread / pthread_create] F1 --> G1[分配内查对象
1-8 MB 内存] G1 --> H1[线程到场调治队列] H1 --> I1[等待 OS 调治] I1 --> J1[开始实行用户代码] J1 --> K1[实行完成] K1 --> L1[线程烧毁
开释 OS 资源] style A1 fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px style G1 fill:#ffccbc,color:#d84315,stroke:#e64a19,stroke-width:2px style L1 fill:#ef9a9a,color:#c62828,stroke:#d32f2f,stroke-width:2px流程 2:Task.Run() 的完备流程
flowchart TD A2[用户代码: Task.Run] --> B2[创建 Task 对象
托管内存: 约 200 bytes] B2 --> C2[TaskScheduler.QueueTask] C2 --> D2[进入 ThreadPool 队列] D2 --> E2{线程池有空闲线程?} E2 -->|是| F2[从池中取出线程
无需创建新线程] E2 -->|否| G2[查抄是否到达 MaxThreads] G2 -->|未到达| H2[Hill Climbing 算法
决定是否创建新线程] G2 -->|已到达| I2[使命列队等待] H2 --> J2[创建新线程
类似 new Thread 流程] F2 --> K2[线程实行 Task] J2 --> K2 I2 --> K2 K2 --> L2[Task 完成] L2 --> M2[线程归还池中
不烧毁,等待复用] style A2 fill:#bbdefb,color:#1565c0,stroke:#1976d2,stroke-width:2px style M2 fill:#c8e6c9,color:#2e7d32,stroke:#388e3c,stroke-width:2px style F2 fill:#fff9c4,color:#f57f17,stroke:#f9a825,stroke-width:2px关键差别对比
特性new Thread()Task.Run()托管对象巨细约 100 bytes约 200 bytes是否创建 OS 线程✅ 每次都创建⚠️ 复用线程池内存开销1-8 MB(每个线程)共享线程池创建时间约 50-200 微秒约 1-5 微秒烧毁时间约 50-200 微秒0(线程不烧毁)实用场景恒久背景使命✅ 大部门场景资源接纳线程烧毁,开释 OS 资源线程归还池,等待复用扩展性差(线程数目有限)✅ 好(复用机制)焦点区别:- new Thread():
- 1. 每次都创建新的 OS 线程
- 2. 消耗 1-8 MB 内存
- 3. 执行完成后销毁线程
- 4. 无法复用
- Task.Run():
- 1. 优先从线程池复用线程
- 2. 只在必要时创建新线程
- 3. 执行完成后线程归还池中
- 4. 高效复用
复制代码 4.3 Task 的两种实行路径
路径 1:CPU 麋集型(利用 ThreadPool 线程)
- 用户代码
- ↓
- Task.Run(() => ComputePrimes())
- ↓
- 进入 ThreadPool 工作队列
- ↓
- ThreadPool 线程执行 lambda
- ↓
- 占用线程,直到计算完成
复制代码 路径 2:I/O 麋集型(不占用线程)
- 用户代码
- ↓
- await httpClient.GetAsync(url)
- ↓
- 发起异步 I/O 请求(OS 级别)
- ↓
- 线程立即释放(返回线程池)
- ↓
- (等待网络响应,不占用线程)
- ↓
- I/O 完成端口(IOCP)收到响应
- ↓
- ThreadPool I/O 线程处理回调
- ↓
- 从线程池取一个线程继续执行后续代码
复制代码 4.4 完备流程图:从 async/await 到 Thread
- 用户代码:
- async Task ProcessAsync()
- {
- await Task.Run(() => ComputePrimes());
- }
- 流程:
- 1. 编译器生成状态机(AsyncMethodBuilder)
- 2. await Task.Run(...) 创建 Task 对象
- 3. Task.Run 调用 TaskScheduler.Default.QueueTask
- 4. ThreadPoolTaskScheduler 调用 ThreadPool.QueueUserWorkItem
- 5. ThreadPool 将任务放入工作队列
- 6. 某个线程池线程(Thread)从队列取出任务
- 7. 线程执行 ComputePrimes()
- 8. 完成后,Task 状态变为 RanToCompletion
- 9. 状态机恢复执行后续代码
复制代码 📊 实战对比
实战 1:创建 10000 个 Thread vs ThreadPool
- // 代码示例:ThreadVsThreadPoolDemo.cs
- // 见文章开头的实验
复制代码 实战 2:ThreadPool 监控
- // 代码示例:ThreadPoolMonitorDemo.cs
- static void MonitorThreadPool()
- {
- var timer = new System.Timers.Timer(1000);
- timer.Elapsed += (s, e) =>
- {
- ThreadPool.GetAvailableThreads(out int worker, out int io);
- ThreadPool.GetMinThreads(out int minWorker, out int minIo);
- ThreadPool.GetMaxThreads(out int maxWorker, out int maxIo);
-
- Console.WriteLine($"可用工作线程:{worker}/{maxWorker}");
- Console.WriteLine($"可用 I/O 线程:{io}/{maxIo}");
- Console.WriteLine($"当前线程数:{ThreadPool.ThreadCount}");
- Console.WriteLine("---");
- };
- timer.Start();
-
- // 提交一些任务
- for (int i = 0; i < 100; i++)
- {
- ThreadPool.QueueUserWorkItem(_ => Thread.Sleep(5000));
- }
-
- Console.ReadLine();
- }
复制代码 实战 3:.NET Framework vs .NET Core 性能对比
- // 需要分别在两个项目中运行
- // 项目 1:Threads(.NET 10)
- // 项目 2:ThreadsFramework(.NET Framework 4.8)
- static void BenchmarkThreadPool()
- {
- const int taskCount = 100000;
- var sw = Stopwatch.StartNew();
-
- var countdown = new CountdownEvent(taskCount);
- for (int i = 0; i < taskCount; i++)
- {
- ThreadPool.QueueUserWorkItem(_ =>
- {
- // 简单工作
- var result = Math.Sqrt(42);
- countdown.Signal();
- });
- }
-
- countdown.Wait();
- sw.Stop();
-
- Console.WriteLine($"完成 {taskCount} 个任务:{sw.ElapsedMilliseconds}ms");
- }
- // 预期结果:
- // .NET Framework 4.8:约 2000ms
- // .NET Core / .NET 10:约 500ms(快 4 倍!)
复制代码 🔥 常见误区澄清
误区 1:"Task 就是线程"
错误认知:- var task = Task.Run(() => DoWork());
- // 认为:创建了一个新线程
复制代码 原形:- // Task = 异步操作的抽象
- // 可能使用 ThreadPool 线程(CPU 密集型)
- // 可能不占用线程(I/O 密集型)
- // CPU 密集型:使用线程池线程
- var task1 = Task.Run(() => ComputePrimes());
- // I/O 密集型:不占用线程
- var task2 = Task.Run(async () => await httpClient.GetAsync(url));
复制代码 误区 2:"Task.Run 会创建新线程"
错误认知:- for (int i = 0; i < 10000; i++)
- {
- Task.Run(() => DoWork());
- }
- // 认为:创建了 10000 个线程
复制代码 原形:- // Task.Run 只是将任务放入 ThreadPool 队列
- // 实际使用的线程数 = ThreadPool.ThreadCount(约 10-100 个)
- // 10000 个 Task 会排队等待执行
复制代码 误区 3:"async 会创建新线程"
错误认知:- async Task ProcessAsync()
- {
- await Task.Delay(1000);
- }
- // 认为:async 关键字会创建线程
复制代码 原形:- // async 只是语法糖,生成状态机
- // 不会创建线程
- // await Task.Delay 使用 OS 定时器,不占用线程
复制代码 🎯 最佳实践发起
1. 优先利用 Task 而不是 Thread
- // ❌ 不推荐
- new Thread(() => DoWork()).Start();
- // ✅ 推荐
- Task.Run(() => DoWork());
复制代码 2. I/O 麋集型利用 async/await
- // ❌ 不推荐(阻塞线程)
- var data = httpClient.GetStringAsync(url).Result;
- // ✅ 推荐(不阻塞线程)
- var data = await httpClient.GetStringAsync(url);
复制代码 3. 不要耗尽 ThreadPool
- // ❌ 不推荐(耗尽线程池)
- for (int i = 0; i < 10000; i++)
- {
- Task.Run(() => Thread.Sleep(10000)); // 长期占用
- }
- // ✅ 推荐(使用 SemaphoreSlim 限流)
- var semaphore = new SemaphoreSlim(10); // 最多 10 个并发
- for (int i = 0; i < 10000; i++)
- {
- await semaphore.WaitAsync();
- Task.Run(async () =>
- {
- try
- {
- await DoWorkAsync();
- }
- finally
- {
- semaphore.Release();
- }
- });
- }
复制代码 4. 监控 ThreadPool 康健度
- // 定期检查
- ThreadPool.GetAvailableThreads(out int worker, out int io);
- if (worker < 10)
- {
- Console.WriteLine("警告:线程池即将耗尽!");
- }
复制代码 📚 总结
焦点要点
- Thread:
- OS 级别的实行单位
- 创建/烧毁资源高(约 1 MB 内存)
- 上下文切换资源高
- 当代开发中很少直接利用
- ThreadPool:
- 线程复用机制
- .NET Core 重构:工作盗取算法,性能提升明显
- 动态调解线程数(Hill Climbing)
- Task 的底层底子
- Task:
- 异步利用的抽象
- CPU 麋集型:利用 ThreadPool 线程
- I/O 麋集型:不占用线程
- 当代 .NET 并发编程的首选
三者关系
- Thread(基础)
- ↓
- ThreadPool(优化)
- ↓
- Task(抽象)
- ↓
- async/await(语法糖)
复制代码 下一章预报
在第三章《Task API 完全指南》中,我们将深入探究:
- Task 的创建、等待、组合 API
- Task.WhenAll vs Task.WhenAny
- ContinueWith 的陷阱
- Task 的状态和非常处置处罚
🔗 参考资料
官方文档
- Microsoft Docs: Managed Threading
- Microsoft Docs: ThreadPool
- Microsoft Docs: Task
源码
- CoreCLR ThreadPool 源码
- CoreCLR Task 源码
深度文章
- Stephen Toub: ThreadPool 内部机制
- .NET Blog: ThreadPool 改进
以为有资助?别忘了 ⭐ Star 本堆栈!
免责声明:如果侵犯了您的权益,请联系站长及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金. |