02-并发的底层-Thread-ThreadPool-Task [复制链接]
发表于 昨天 11:24 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
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 个线程
  1. // 代码示例:ThreadVsTaskDemo.cs
  2. private static void CreateTenThousandThreads()
  3. {
  4.     const int threadCount = 10000;
  5.     var sw = Stopwatch.StartNew();
  6.     var threads = new Thread[threadCount];
  7.     Console.WriteLine($"开始创建 {threadCount} 个线程...");
  8.     for (int i = 0; i < threadCount; i++)
  9.     {
  10.         threads[i] = new Thread(() =>
  11.         {
  12.             Thread.Sleep(5000);  // 模拟工作 5 秒
  13.         });
  14.         threads[i].Start();
  15.         if ((i + 1) % 1000 == 0)
  16.         {
  17.             Console.WriteLine($"  已创建 {i + 1} 个线程...");
  18.         }
  19.     }
  20.     Console.WriteLine("所有线程已启动,等待完成...");
  21.     foreach (var thread in threads)
  22.     {
  23.         thread.Join();
  24.     }
  25.     sw.Stop();
  26.     Console.WriteLine($"✓ 完成时间:{sw.ElapsedMilliseconds}ms");
  27.     Console.WriteLine($"✓ 创建的线程数:{threadCount} 个");
  28.     Console.WriteLine($"✓ 虚拟内存预留:约 {threadCount / 1024.0:F2} GB(每线程 1MB 栈空间)");
  29.     Console.WriteLine($"✓ 实际物理内存:请查看任务管理器(约 1-2 GB,因为栈未被充分使用)");
  30.     Console.WriteLine();
  31.     Console.WriteLine("说明:");
  32.     Console.WriteLine("  - Windows 为每个线程预留 1MB 虚拟地址空间");
  33.     Console.WriteLine("  - 但只有栈被实际访问时,才分配物理内存(4KB 页)");
  34.     Console.WriteLine("  - 当前代码仅调用 Thread.Sleep(),栈使用很少");
  35.     Console.WriteLine("  - 若要查看虚拟内存占用,请使用 Process Explorer 工具");
  36. }
复制代码
运行效果
  1. 开始创建 10000 个线程...
  2.   已创建 1000 个线程...
  3.   已创建 2000 个线程...
  4.   ...
  5.   已创建 10000 个线程...
  6. 所有线程已启动,等待完成...
  7. ✓ 完成时间:约 5000-10000ms(取决于系统)
  8. ✓ 创建的线程数:10000 个
  9. ✓ 虚拟内存预留:约 9.77 GB(每线程 1MB 栈空间)
  10. ✓ 实际物理内存:约 1-2 GB(任务管理器显示)
  11. 说明:
  12.   - Windows 为每个线程预留 1MB 虚拟地址空间
  13.   - 但只有栈被实际访问时,才分配物理内存(4KB 页)
  14.   - 当前代码仅调用 Thread.Sleep(),栈使用很少
  15.   - 若要查看虚拟内存占用,请使用 Process Explorer 工具
复制代码
💡 关于内存占用的紧张阐明
你大概会注意到:使命管理器表现的内存只有 1-2GB,而不是理论的 10GB。这是正常征象!
为什么?

  • 假造内存(预留):10GB —— 这是利用体系为全部线程栈预留的假造地点空间
  • 物理内存(实际):1-2GB —— 这是实际分配的物理内存
关键机制:按需分配(Demand Paging)

  • Windows 为每个线程预留 1MB 假造地点空间(VirtualAlloc)
  • 但只有当栈空间被实际访问时,才分配物理内存(4KB 页为单位)
  • 本实行的线程只调用 Thread.Sleep(),险些不利用栈空间
  • 没有局部变量、没有深度函数调用 → 栈利用少少 → 物理内存分配少少
真实场景对比:
  1. 简单场景(本实验):
  2.   - 虚拟:10GB
  3.   - 物理:1-2GB
  4.   - 原因:栈使用极少
  5. 复杂场景(实际应用):
  6.   - 虚拟:10GB
  7.   - 物理:5-10GB
  8.   - 原因:深度调用栈、大量局部变量
复制代码
本实行的真正重点:

  • 线程数目过多:10000 个 OS 线程的创建和管理资源
  • 上下文切换频仍:大量线程导致 CPU 浪费在调治上
  • ⚠️ 内存不是焦点题目:固然假造内存很大,但物理内存斲丧取决于实际利用
要观察假造内存?利用 Process Explorer:

  • 下载:Windows Sysinternals - Process Explorer
  • 查察列:Virtual Size(假造内存) vs Working Set(物理内存)
其他性能题目:

  • CPU 上下文切换:频仍
  • 体系相应:卡顿/瓦解
  • 线程创建时间:50-200 微秒/线程
实行 2:创建 10000 个 Task
  1. // 代码示例:ThreadVsTaskDemo.cs
  2. private static async Task CreateTenThousandTasksAsync()
  3. {
  4.     const int taskCount = 10000;
  5.     var sw = Stopwatch.StartNew();
  6.     var tasks = new Task[taskCount];
  7.     Console.WriteLine($"开始创建 {taskCount} 个 Task...");
  8.     // 记录初始线程数
  9.     var initialThreadCount = ThreadPool.ThreadCount;
  10.     for (int i = 0; i < taskCount; i++)
  11.     {
  12.         tasks[i] = Task.Run(async () =>
  13.         {
  14.             await Task.Delay(5000);  // I/O 密集型:不占用线程
  15.         });
  16.     }
  17.     // 等待一下让任务启动
  18.     await Task.Delay(100);
  19.     // 查看运行时的线程数
  20.     var runningThreadCount = ThreadPool.ThreadCount;
  21.     Console.WriteLine("所有 Task 已启动,等待完成...");
  22.     await Task.WhenAll(tasks);
  23.     sw.Stop();
  24.     Console.WriteLine($"✓ 完成时间:{sw.ElapsedMilliseconds}ms");
  25.     Console.WriteLine($"✓ 实际使用线程数:约 {runningThreadCount}(初始:{initialThreadCount})");
  26.     Console.WriteLine($"✓ Task 对象内存:约 {taskCount * 200 / 1024.0 / 1024.0:F2} MB");
  27.     Console.WriteLine($"✓ 性能优势:线程数减少约 {taskCount / Math.Max(runningThreadCount, 1)}x");
  28.     // 对比说明
  29.     Console.WriteLine($"\n对比分析:");
  30.     Console.WriteLine($"  - 如果用 {taskCount} 个 Thread:内存约 {taskCount / 1024.0:F2} GB");
  31.     Console.WriteLine($"  - 实际用 Task:内存约 {taskCount * 200 / 1024.0 / 1024.0:F2} MB");
  32.     Console.WriteLine($"  - 内存节省:约 {(taskCount / 1024.0 * 1024) / (taskCount * 200 / 1024.0):F0}x");
  33. }
复制代码
运行效果
  1. 开始创建 10000 个 Task...
  2. 所有 Task 已启动,等待完成...
  3. ✓ 完成时间:约 5000ms
  4. ✓ 实际使用线程数:约 10-20(初始:4-8)
  5. ✓ Task 对象内存:约 1.91 MB
  6. ✓ 性能优势:线程数减少约 500-1000x
  7. 对比分析:
  8.   - 如果用 10000 个 Thread:内存约 9.77 GB(虚拟内存)
  9.   - 实际用 Task:内存约 1.91 MB(Task 对象)
  10.   - 内存节省:约 5000x
复制代码
🤔 为什么差距云云巨大?

这个实行展现了一个焦点题目:
  1. Thread:
  2.   - 每个 Thread = 1 个 OS 线程
  3.   - 10000 个 Thread = 10000 个 OS 线程
  4.   - 虚拟内存开销:约 10GB(预留)
  5.   - 物理内存开销:1-10GB(取决于实际使用)
  6.   - 上下文切换:频繁(10000 个线程竞争 CPU)
  7.   - 创建/销毁:昂贵(每个线程 50-200 微秒)
  8. Task:
  9.   - Task ≠ 线程
  10.   - Task = 异步操作的抽象
  11.   - 10000 个 Task 可能只用 10-20 个线程
  12.   - 内存开销:约 2 MB(Task 对象)
  13.   - 上下文切换:少(只有 10-20 个线程)
  14.   - 复用线程池:无创建/销毁成本
复制代码
引发思索
  1. 1. Thread 和 Task 到底是什么关系?
  2. 2. ThreadPool 是如何优化线程使用的?
  3. 3. Task 是如何实现"用少量线程处理大量任务"的?
  4. 4. 为什么实际物理内存远小于虚拟内存?(按需分配机制)
  5.   - 上下文切换:少
  6.   - 复用线程池
复制代码
带着这些题目,我们开始深入探索 .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)
  1. // 用户代码
  2. var thread = new Thread(() =>
  3. {
  4.     Console.WriteLine("Hello from thread!");
  5. });
  6. // 对应的底层操作:
  7. // 1. CLR 分配 Thread 对象(托管堆)
  8. // 2. 初始化 Thread 对象的字段:
  9. //    - m_Delegate(要执行的委托)
  10. //    - m_ThreadStart(线程入口点)
  11. //    - m_ThreadId(初始为 0)
  12. //    - m_ThreadState(Unstarted)
复制代码
第二阶段:启动线程
  1. thread.Start();  // ← 这一步才真正创建 OS 线程
  2. // 底层流程:
  3. // CLR 内部调用栈:
  4. Thread.Start()
  5.   └─ StartInternal()
  6.       └─ ThreadNative::Start()  // C++ 实现
  7.           └─ P/Invoke
  8.               └─ CreateThread (Windows)
  9.               └─ pthread_create (Linux)
复制代码
第三阶段:利用体系层

Windows 体系
  1. // 伪代码:操作系统内核做了什么
  2. HANDLE CreateThread(
  3.     LPSECURITY_ATTRIBUTES lpThreadAttributes,
  4.     SIZE_T dwStackSize,              // 栈大小,默认 1 MB
  5.     LPTHREAD_START_ROUTINE lpStartAddress,
  6.     LPVOID lpParameter,
  7.     DWORD dwCreationFlags,
  8.     LPDWORD lpThreadId
  9. )
  10. {
  11.     // 1. 分配内核线程对象(KTHREAD)
  12.     PKTHREAD kthread = AllocateKernelThreadObject();
  13.     // 2. 分配用户态栈空间(默认 1 MB)
  14.     PVOID stackBase = VirtualAlloc(NULL, 1 * 1024 * 1024, ...);
  15.     // 3. 创建线程环境块(TEB)
  16.     PTEB teb = CreateThreadEnvironmentBlock();
  17.     teb->StackBase = stackBase;
  18.     teb->StackLimit = stackBase + stackSize;
  19.     // 4. 初始化线程上下文(寄存器、栈指针等)
  20.     CONTEXT context;
  21.     InitializeContext(&context, lpStartAddress, stackBase);
  22.     // 5. 将线程加入调度器的就绪队列
  23.     KeReadyThread(kthread);
  24.     // 6. 返回线程句柄
  25.     return CreateHandle(kthread);
  26. }
复制代码
Linux 体系
  1. // 伪代码:pthread_create 的内部实现
  2. int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
  3.                    void *(*start_routine) (void *), void *arg)
  4. {
  5.     // 1. 分配线程描述符(task_struct)
  6.     struct task_struct *new_task = alloc_task_struct();
  7.     // 2. 分配栈空间(默认 8 MB,可配置
  8.     void *stack = mmap(NULL, PTHREAD_STACK_MIN, ...);
  9.     // 3. 复制父线程的资源(文件描述符、信号等)
  10.     copy_process(new_task, CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND);
  11.     // 4. 设置线程入口点和参数
  12.     new_task->thread.sp = (unsigned long)stack + PTHREAD_STACK_MIN;
  13.     new_task->thread.ip = (unsigned long)start_routine;
  14.     // 5. 唤醒线程(加入调度队列)
  15.     wake_up_new_task(new_task);
  16.     return 0;
  17. }
复制代码
第四阶段:内存分配详情

每个线程的内存占用
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第五阶段:线程调治
  1. 时间轴:
  2. 0 ms:  thread.Start() 调用
  3.   ↓
  4. 5 ms:  OS 创建线程对象完成
  5.   ↓
  6. 10 ms: 线程进入就绪队列
  7.   ↓
  8. ???:   等待 CPU 时间片(取决于系统负载
  9.   ↓
  10. 15 ms: OS 调度器分配 CPU
  11.   ↓
  12. 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 物理内存
  1. 虚拟内存(Virtual Memory):
  2.   - 操作系统为线程栈预留的地址空间
  3.   - Windows:VirtualAlloc() 预留 1 MB
  4.   - Linux:mmap() 预留 8 MB
  5.   - 不占用物理 RAM,仅占用地址空间
  6. 物理内存(Physical Memory / RAM):
  7.   - 实际分配的 RAM 页(4KB 为单位)
  8.   - 只有当栈被访问时才分配(Demand Paging)
  9.   - 简单代码(如 Thread.Sleep)仅使用几 KB
  10.   - 复杂代码(深度递归、大量局部变量)可能使用接近 1 MB
复制代码
盘算示例(假造内存预留)
  1. Windows(每个线程预留 1 MB 栈空间):
  2.   1000 个线程 = 1000 MB ≈ 1 GB(虚拟内存)
  3.   10000 个线程 = 10000 MB ≈ 10 GB(虚拟内存)
  4.   但物理内存:
  5.   - 简单代码:1000 个线程 ≈ 8-16 MB
  6.   - 复杂代码:1000 个线程 ≈ 500 MB - 1 GB
  7. Linux(每个线程预留 8 MB 栈空间):
  8.   1000 个线程 = 8000 MB ≈ 8 GB(虚拟内存)
  9.   10000 个线程 = 80000 MB ≈ 78 GB(虚拟内存)
  10.   但物理内存类似 Windows(取决于实际使用)
复制代码
💡 为什么会有这个差别?
利用体系利用按需分配(Demand Paging)战略:

  • 创建线程时,预留假造地点空间(VirtualAlloc/mmap)
  • 栈空间被访问时,才分配物理内存(4KB 页)
  • 本章开头实行中的 Thread.Sleep() 险些不利用栈 → 物理内存很少
  • 实际应用中的深度调用栈会利用更多物理内存
观察方法

  • 使命管理器:表现工作集(Working Set) = 物理内存
  • Process Explorer:可查察假造巨细(Virtual Size) = 假造内存
1.2 Thread 的上下文切换资源

什么是上下文切换?

上下文切换(Context Switch) 是指 CPU 从一个线程切换到另一个线程的过程。这是多线程并发的底子,但也是性能开销的重要泉源。
简化流程
  1. 线程 A 正在运行
  2.     ↓
  3. 时间片用完(或 I/O 阻塞)
  4.     ↓
  5. 保存 A 的状态(寄存器、栈指针、PC)
  6.     ↓
  7. 加载线程 B 的状态
  8.     ↓
  9. 线程 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页表缓存数百条时间分解
  1. 保存寄存器状态:      约 0.5-1 微秒
  2. 运行调度算法:        约 0.5-2 微秒
  3. 加载新线程状态:      约 0.5-1 微秒
  4. 刷新 TLB:           约 0.5-1 微秒
  5. 进入/退出内核态:     约 0.5-1 微秒
  6. ────────────────────────────────────
  7. 总计:                约 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性能影响
  1. 假设:
  2. - CPU 频率:3 GHz(每时钟周期约 0.33 纳秒)
  3. - L1 缓存命中率(无上下文切换):95%
  4. - L1 缓存命中率(频繁上下文切换):50%
  5. 无上下文切换:
  6.   95% × 4 周期 + 5% × 200 周期 = 13.8 周期 ≈ 4.6 纳秒
  7. 频繁上下文切换:
  8.   50% × 4 周期 + 50% × 200 周期 = 102 周期 ≈ 34 纳秒
  9. 性能下降:34 / 4.6 ≈ 7.4 倍!
复制代码
3. TLB(Translation Lookaside Buffer)失效

TLB 的作用

  • 缓存假造地点到物理地点的映射
  • 克制每次内存访问都查页表
上下文切换的影响
  1. 线程 A → 线程 B
  2.     ↓
  3. TLB 中存储的是线程 A 的地址映射
  4.     ↓
  5. 线程 B 需要不同的地址映射
  6.     ↓
  7. TLB 失效(Flush)
  8.     ↓
  9. 线程 B 的每次内存访问都需要查页表
  10.     ↓
  11. 延迟增加:从 1 个时钟周期 → 10-100 个时钟周期
复制代码
4. 指令流水线停顿

CPU 流水线
  1. 指令 1: 取指 → 解码 → 执行 → 访存 → 写回
  2. 指令 2:       取指 → 解码 → 执行 → 访存 → 写回
  3. 指令 3:             取指 → 解码 → 执行 → 访存 → 写回
复制代码
上下文切换导致流水线清空
  1. 线程 A 执行中:
  2.   流水线:[指令5][指令4][指令3][指令2][指令1]
  3.     ↓
  4. 上下文切换
  5.     ↓
  6. 流水线清空:[   ][   ][   ][   ][   ]
  7.     ↓
  8. 线程 B 开始:
  9.   重新填充流水线:[指令1][   ][   ][   ][   ]
  10.     ↓
  11. 浪费:约 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%,间接资源(缓存失效)才是重要开销
实行:丈量上下文切换资源
  1. // 代码示例:ContextSwitchCostDemo.cs
  2. static void MeasureContextSwitchCost()
  3. {
  4.     const int iterations = 1_000_000;
  5.    
  6.     // 单线程基准测试
  7.     var sw1 = Stopwatch.StartNew();
  8.     for (int i = 0; i < iterations; i++)
  9.     {
  10.         // 简单计算
  11.         var result = i * i;
  12.     }
  13.     sw1.Stop();
  14.     Console.WriteLine($"单线程:{sw1.ElapsedMilliseconds}ms");
  15.    
  16.     // 多线程(强制上下文切换)
  17.     var sw2 = Stopwatch.StartNew();
  18.     var threads = new Thread[10];
  19.     for (int i = 0; i < 10; i++)
  20.     {
  21.         threads[i] = new Thread(() =>
  22.         {
  23.             for (int j = 0; j < iterations / 10; j++)
  24.             {
  25.                 var result = j * j;
  26.                 Thread.Sleep(0);  // 强制上下文切换
  27.             }
  28.         });
  29.         threads[i].Start();
  30.     }
  31.     foreach (var t in threads) t.Join();
  32.     sw2.Stop();
  33.     Console.WriteLine($"多线程(频繁切换):{sw2.ElapsedMilliseconds}ms");
  34. }
复制代码
预期效果
  1. 单线程:约 5ms
  2. 多线程(频繁切换):约 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 字节)。
  1. 缓存行结构(x86/x64 典型值:64 字节):
  2. ┌────────────────────────────────────────────────────┐
  3. │ 字节 0-7 │ 字节 8-15 │ ... │ 字节 56-63 │
  4. │  数据 A  │   数据 B  │ ... │   数据 H   │
  5. └────────────────────────────────────────────────────┘
复制代码
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 的性能影响
  1. // 代码示例:FalseSharingDemo.cs
  2. // ❌ 有 False Sharing 的版本
  3. class BadCounter
  4. {
  5.     public long Counter1;  // 8 字节
  6.     public long Counter2;  // 8 字节(在同一缓存行中!)
  7. }
  8. static void DemonstrateFalseSharing()
  9. {
  10.     var bad = new BadCounter();
  11.     var sw = Stopwatch.StartNew();
  12.     var t1 = new Thread(() =>
  13.     {
  14.         for (int i = 0; i < 100_000_000; i++)
  15.         {
  16.             bad.Counter1++;  // 线程 1 修改 Counter1
  17.         }
  18.     });
  19.     var t2 = new Thread(() =>
  20.     {
  21.         for (int i = 0; i < 100_000_000; i++)
  22.         {
  23.             bad.Counter2++;  // 线程 2 修改 Counter2
  24.         }
  25.     });
  26.     t1.Start();
  27.     t2.Start();
  28.     t1.Join();
  29.     t2.Join();
  30.     sw.Stop();
  31.     Console.WriteLine($"有 False Sharing:{sw.ElapsedMilliseconds}ms");
  32. }
  33. // ✅ 避免 False Sharing 的版本
  34. [StructLayout(LayoutKind.Explicit)]
  35. class GoodCounter
  36. {
  37.     [FieldOffset(0)]
  38.     public long Counter1;   // 偏移 0
  39.     // 填充 56 字节(64 - 8 = 56)
  40.     [FieldOffset(64)]
  41.     public long Counter2;   // 偏移 64(在下一个缓存行)
  42. }
  43. static void DemonstrateNoPadding()
  44. {
  45.     var good = new GoodCounter();
  46.     var sw = Stopwatch.StartNew();
  47.     var t1 = new Thread(() =>
  48.     {
  49.         for (int i = 0; i < 100_000_000; i++)
  50.         {
  51.             good.Counter1++;
  52.         }
  53.     });
  54.     var t2 = new Thread(() =>
  55.     {
  56.         for (int i = 0; i < 100_000_000; i++)
  57.         {
  58.             good.Counter2++;
  59.         }
  60.     });
  61.     t1.Start();
  62.     t2.Start();
  63.     t1.Join();
  64.     t2.Join();
  65.     sw.Stop();
  66.     Console.WriteLine($"无 False Sharing:{sw.ElapsedMilliseconds}ms");
  67. }
复制代码
实际运行效果
  1. 有 False Sharing:约 8000ms
  2. 无 False Sharing:约 800ms
  3. 性能提升:10 倍!
复制代码
False Sharing 的办理方案

方案 1:添补(Padding)
  1. class PaddedCounter
  2. {
  3.     public long Counter1;
  4.     // 填充 56 字节,确保 Counter2 在下一个缓存行
  5.     private long _padding1, _padding2, _padding3, _padding4, _padding5, _padding6, _padding7;
  6.     public long Counter2;
  7. }
复制代码
方案 2:利用 [StructLayout] 特性
  1. [StructLayout(LayoutKind.Explicit, Size = 128)]  // 两个缓存行
  2. struct CacheLine
  3. {
  4.     [FieldOffset(0)]
  5.     public long Counter1;
  6.     [FieldOffset(64)]  // 64 字节后,确保在下一个缓存行
  7.     public long Counter2;
  8. }
复制代码
方案 3:利用线程本地存储(Thread-Local Storage)
  1. class ThreadLocalCounter
  2. {
  3.     private ThreadLocal<long> _counter = new(() => 0);
  4.     public void Increment()
  5.     {
  6.         _counter.Value++;
  7.     }
  8.     public long GetTotal()
  9.     {
  10.         // 最后合并所有线程的计数
  11.         return _counter.Values.Sum();
  12.     }
  13. }
复制代码
实际应用中的 False Sharing 案例

案例 1:数组元素访问
  1. // ❌ 错误:多个线程修改相邻数组元素
  2. long[] counters = new long[4];
  3. Parallel.For(0, 4, i =>
  4. {
  5.     for (int j = 0; j < 1000000; j++)
  6.     {
  7.         counters[i]++;  // False Sharing!
  8.     }
  9. });
  10. // ✅ 正确:使用填充
  11. long[] counters = new long[4 * 8];  // 每个计数器占 8 个 long(64 字节)
  12. Parallel.For(0, 4, i =>
  13. {
  14.     for (int j = 0; j < 1000000; j++)
  15.     {
  16.         counters[i * 8]++;  // 每个计数器相隔 64 字节
  17.     }
  18. });
复制代码
案例 2:多线程计数器
  1. // .NET 的 Interlocked 类已经考虑了 False Sharing
  2. // 但自定义计数器需要注意
  3. // ❌ 错误
  4. class MultiCounter
  5. {
  6.     public int Count1;
  7.     public int Count2;
  8.     public int Count3;
  9.     public int Count4;
  10. }
  11. // ✅ 正确
  12. [StructLayout(LayoutKind.Explicit)]
  13. class PaddedMultiCounter
  14. {
  15.     [FieldOffset(0)]
  16.     public int Count1;
  17.     [FieldOffset(64)]
  18.     public int Count2;
  19.     [FieldOffset(128)]
  20.     public int Count3;
  21.     [FieldOffset(192)]
  22.     public int Count4;
  23. }
复制代码
常见导致上下文切换的场景

相识哪些利用会触发上下文切换,可以资助我们编写更高效的多线程代码。
1. 壅闭 I/O 利用
  1. // ❌ 阻塞式 I/O:导致上下文切换
  2. void BlockingIo()
  3. {
  4.     // 线程阻塞,等待网络响应
  5.     var response = httpClient.GetStringAsync(url).Result;
  6.     // 线程阻塞,等待文件读取
  7.     var data = File.ReadAllText(filePath);
  8. }
  9. // ✅ 异步 I/O:不阻塞线程
  10. async Task AsyncIo()
  11. {
  12.     // 线程释放,不阻塞
  13.     var response = await httpClient.GetStringAsync(url);
  14.     // 线程释放,不阻塞
  15.     var data = await File.ReadAllTextAsync(filePath);
  16. }
  17. // 性能影响:
  18. // - 阻塞 I/O:线程等待期间发生上下文切换(浪费线程资源)
  19. // - 异步 I/O:线程立即释放,可以处理其他任务
复制代码
2. 锁竞争(Lock Contention)
  1. // ❌ 高锁竞争:频繁上下文切换
  2. object _lock = new();
  3. void HighContention()
  4. {
  5.     // 100 个线程竞争同一个锁
  6.     Parallel.For(0, 100, i =>
  7.     {
  8.         lock (_lock)  // 大部分线程会阻塞,等待锁释放
  9.         {
  10.             // 临界区
  11.             Thread.Sleep(10);  // 模拟工作
  12.         }
  13.     });
  14. }
  15. // ✅ 减少锁粒度:降低竞争
  16. class ShardedLock
  17. {
  18.     private readonly object[] _locks = new object[16];
  19.     public ShardedLock()
  20.     {
  21.         for (int i = 0; i < _locks.Length; i++)
  22.         {
  23.             _locks[i] = new object();
  24.         }
  25.     }
  26.     public void Execute(int id, Action action)
  27.     {
  28.         // 根据 ID 选择不同的锁(减少竞争)
  29.         var lockIndex = id % _locks.Length;
  30.         lock (_locks[lockIndex])
  31.         {
  32.             action();
  33.         }
  34.     }
  35. }
  36. // 性能影响:
  37. // - 高竞争:大量线程阻塞 → 频繁上下文切换 → 性能下降 50-90%
  38. // - 分片锁:竞争减少 → 上下文切换减少 → 性能提升 5-10 倍
复制代码
3. Thread.Sleep() 和 Thread.Yield()
  1. // ❌ Thread.Sleep(0):主动触发上下文切换
  2. void ForceContextSwitch()
  3. {
  4.     for (int i = 0; i < 1000000; i++)
  5.     {
  6.         DoWork();
  7.         Thread.Sleep(0);  // 强制上下文切换
  8.     }
  9. }
  10. // ⚠️ Thread.Sleep(n):线程休眠,触发上下文切换
  11. void SleepExample()
  12. {
  13.     Thread.Sleep(100);  // 线程休眠 100ms,让出 CPU
  14. }
  15. // ⚠️ Thread.Yield():主动让出 CPU 时间片
  16. void YieldExample()
  17. {
  18.     Thread.Yield();  // 让出当前时间片给其他线程
  19. }
  20. // 性能影响:
  21. // - 频繁 Sleep(0):每次调用都触发上下文切换
  22. // - 实验结果:见上文 ContextSwitchCostDemo(慢 100 倍)
复制代码
4. 等待利用(Wait / Join)
  1. // ❌ 同步等待:线程阻塞
  2. void SyncWait()
  3. {
  4.     var task = Task.Run(() => LongRunningWork());
  5.     task.Wait();  // 线程阻塞,触发上下文切换
  6. }
  7. void ThreadJoin()
  8. {
  9.     var thread = new Thread(() => LongRunningWork());
  10.     thread.Start();
  11.     thread.Join();  // 线程阻塞,触发上下文切换
  12. }
  13. // ✅ 异步等待:不阻塞线程
  14. async Task AsyncWait()
  15. {
  16.     var task = Task.Run(() => LongRunningWork());
  17.     await task;  // 不阻塞线程,不触发上下文切换
  18. }
  19. // 性能影响:
  20. // - task.Wait():阻塞当前线程 → 上下文切换 → 浪费线程资源
  21. // - await task:释放当前线程 → 无上下文切换 → 高效利用线程池
复制代码
5. 信号量和事故等待
  1. // ⚠️ ManualResetEvent / AutoResetEvent:阻塞式等待
  2. void EventWait()
  3. {
  4.     var resetEvent = new ManualResetEvent(false);
  5.     var t1 = new Thread(() =>
  6.     {
  7.         resetEvent.WaitOne();  // 线程阻塞,等待信号
  8.         DoWork();
  9.     });
  10.     t1.Start();
  11.     Thread.Sleep(1000);
  12.     resetEvent.Set();  // 唤醒等待的线程(触发上下文切换)
  13. }
  14. // ✅ SemaphoreSlim.WaitAsync():异步等待
  15. async Task SemaphoreWaitAsync()
  16. {
  17.     var semaphore = new SemaphoreSlim(0);
  18.     var task = Task.Run(async () =>
  19.     {
  20.         await semaphore.WaitAsync();  // 不阻塞线程
  21.         DoWork();
  22.     });
  23.     await Task.Delay(1000);
  24.     semaphore.Release();  // 释放信号
  25. }
  26. // 性能影响:
  27. // - WaitOne():阻塞 → 上下文切换(约 2-7 微秒)
  28. // - WaitAsync():不阻塞 → 无上下文切换
复制代码
6. 线程数目过多
  1. // ❌ 过多线程:频繁上下文切换
  2. void TooManyThreads()
  3. {
  4.     for (int i = 0; i < 1000; i++)
  5.     {
  6.         new Thread(() =>
  7.         {
  8.             while (true)
  9.             {
  10.                 DoWork();
  11.                 Thread.Sleep(10);
  12.             }
  13.         }).Start();
  14.     }
  15. }
  16. // ✅ 使用线程池:线程数量受控
  17. void UseThreadPool()
  18. {
  19.     for (int i = 0; i < 1000; i++)
  20.     {
  21.         ThreadPool.QueueUserWorkItem(_ =>
  22.         {
  23.             DoWork();
  24.         });
  25.     }
  26. }
  27. // 性能影响:
  28. // - 1000 个线程 × 100 次切换/秒 = 100000 次上下文切换/秒
  29. // - 每次切换 50 微秒 = 5 秒/秒(50% 时间在切换!)
复制代码
7. 频仍的 Task.Run
  1. // ❌ 过度使用 Task.Run:增加调度开销
  2. async Task OveruseTaskRun()
  3. {
  4.     for (int i = 0; i < 10000; i++)
  5.     {
  6.         await Task.Run(() => DoSmallWork());  // 小任务,频繁调度
  7.     }
  8. }
  9. // ✅ 批量处理:减少调度次数
  10. async Task BatchProcessing()
  11. {
  12.     var tasks = Enumerable.Range(0, 10000)
  13.         .Select(i => Task.Run(() => DoSmallWork()))
  14.         .ToArray();
  15.     await Task.WhenAll(tasks);
  16. }
  17. // 或者:直接在当前线程执行(如果是 CPU 密集型)
  18. void DirectExecution()
  19. {
  20.     for (int i = 0; i < 10000; i++)
  21.     {
  22.         DoSmallWork();  // 如果工作很小,不值得调度
  23.     }
  24. }
  25. // 性能影响:
  26. // - 每次 Task.Run:约 1-5 微秒调度开销
  27. // - 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:内存限定
  1. 假设:
  2. - 机器内存:16 GB
  3. - 每个线程:1 MB
  4. 理论上限:16 GB / 1 MB = 16000 个线程
  5. 实际上限:约 2000-5000 个(OS 保留、其他进程占用)
复制代码
限定 2:调治器性能降落
  1. 线程数量 → 调度开销
  2. 100 个线程:调度开销 < 1%
  3. 1000 个线程:调度开销 约 5-10%
  4. 10000 个线程:调度开销 > 50%(大部分时间在切换!)
复制代码
限定 3:饥饿题目
  1. // 反例:线程饥饿
  2. for (int i = 0; i < 10000; i++)
  3. {
  4.     new Thread(() =>
  5.     {
  6.         // 每个线程都想获取同一个锁
  7.         lock (sharedLock)
  8.         {
  9.             // 临界区
  10.         }
  11.     }).Start();
  12. }
  13. // 问题:
  14. // - 大量线程竞争锁
  15. // - 大部分线程处于等待状态
  16. // - 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. 1. 预先创建一组线程
  3. 2. 任务来了,从池中取一个线程执行
  4. 3. 任务完成后,线程归还池中(不销毁)
  5. 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. // ✅ 场景 1:主线程提交 → 全局队列
  2. static void Main()
  3. {
  4.     // 当前线程:主线程(非 ThreadPool 线程)
  5.     ThreadPool.QueueUserWorkItem(_ =>
  6.     {
  7.         Console.WriteLine("任务 A");
  8.     });
  9.     // ↑ 任务 A 进入全局队列
  10. }
  11. // ✅ 场景 2:ThreadPool 线程内部提交 → 本地队列
  12. static void Main()
  13. {
  14.     ThreadPool.QueueUserWorkItem(_ =>
  15.     {
  16.         // 当前线程:ThreadPool 线程
  17.         Console.WriteLine("任务 A");
  18.         ThreadPool.QueueUserWorkItem(_ =>
  19.         {
  20.             Console.WriteLine("任务 B");
  21.         });
  22.         // ↑ 任务 B 进入当前线程的本地队列(LIFO 无锁)
  23.     });
  24. }
  25. // ✅ 场景 3:Task 内部提交 Task → 本地队列
  26. static async Task ProcessAsync()
  27. {
  28.     await Task.Run(() =>
  29.     {
  30.         // 当前线程:ThreadPool 线程
  31.         Console.WriteLine("任务 A");
  32.         Task.Run(() =>
  33.         {
  34.             Console.WriteLine("任务 B");
  35.         });
  36.         // ↑ 任务 B 进入当前线程的本地队列
  37.     });
  38. }
  39. // ✅ 场景 4:UI 线程提交 → 全局队列
  40. private void Button_Click(object sender, EventArgs e)
  41. {
  42.     // 当前线程:UI 线程(非 ThreadPool 线程)
  43.     Task.Run(() =>
  44.     {
  45.         Console.WriteLine("后台任务");
  46.     });
  47.     // ↑ 任务进入全局队列
  48. }
复制代码
底层实现(简化)
  1. // .NET Core ThreadPool 内部逻辑
  2. public void QueueUserWorkItem(WaitCallback callback)
  3. {
  4.     // 检查当前线程是否是 ThreadPool 线程
  5.     var currentThreadLocalQueue = t_queue;  // [ThreadStatic] 线程本地变量
  6.     if (currentThreadLocalQueue != null)
  7.     {
  8.         // 当前是 ThreadPool 线程 → 放入本地队列
  9.         currentThreadLocalQueue.LocalPush(callback);  // LIFO 无锁
  10.     }
  11.     else
  12.     {
  13.         // 当前不是 ThreadPool 线程 → 放入全局队列
  14.         _globalQueue.Enqueue(callback);  // FIFO 低锁
  15.     }
  16. }
复制代码
性能影响
提交场景目标队列锁开销缓存友爱性性能主线程 → 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)
  1. 线程 2(空闲)→ 等待全局队列有新任务
  2.     ↓
  3. 问题:
  4. - 如果没有新任务提交,线程 2 一直空闲
  5. - 线程 1 的任务完不成,整体吞吐量下降
复制代码
工作盗取办理方案(.NET Core)
  1. 线程 2(空闲)→ 主动从线程 1 的队列中"偷"任务
  2.     ↓
  3. 优点:
  4. - 自动负载均衡
  5. - 提高 CPU 利用率
  6. - 加速任务完成
复制代码
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代码层面的体现
  1. // .NET Core ThreadPool 内部的简化实现(伪代码)
  2. class ThreadPoolWorkQueue
  3. {
  4.     // 每个线程的本地队列(双端队列)
  5.     [ThreadStatic]
  6.     private static WorkStealingQueue? t_queue;
  7.     // 全局队列
  8.     private readonly ConcurrentQueue<IThreadPoolWorkItem> _globalQueue;
  9.     public void Enqueue(IThreadPoolWorkItem item)
  10.     {
  11.         // 如果有本地队列,优先放入本地队列
  12.         if (t_queue != null)
  13.         {
  14.             t_queue.LocalPush(item);  // LIFO:从尾部加入
  15.         }
  16.         else
  17.         {
  18.             // 没有本地队列,放入全局队列
  19.             _globalQueue.Enqueue(item);
  20.         }
  21.     }
  22.     public bool TryDequeue(out IThreadPoolWorkItem? item)
  23.     {
  24.         // 1. 先尝试从本地队列取(LIFO)
  25.         if (t_queue != null && t_queue.LocalPop(out item))
  26.         {
  27.             return true;  // 从尾部取出
  28.         }
  29.         // 2. 本地队列空了,从全局队列取
  30.         if (_globalQueue.TryDequeue(out item))
  31.         {
  32.             return true;
  33.         }
  34.         // 3. 全局队列也空了,尝试"偷"其他线程的任务
  35.         return TrySteal(out item);
  36.     }
  37.     private bool TrySteal(out IThreadPoolWorkItem? item)
  38.     {
  39.         // 遍历所有其他线程的本地队列
  40.         foreach (var queue in AllThreadLocalQueues)
  41.         {
  42.             if (queue != t_queue && queue.TrySteal(out item))
  43.             {
  44.                 return true;  // FIFO:从头部偷取
  45.             }
  46.         }
  47.         item = null;
  48.         return false;
  49.     }
  50. }
  51. class WorkStealingQueue
  52. {
  53.     private IThreadPoolWorkItem[] _array;
  54.     private volatile int _headIndex;  // 头部索引(窃取端)
  55.     private volatile int _tailIndex;  // 尾部索引(本线程端)
  56.     // 本线程 Push(LIFO):从尾部加入
  57.     public void LocalPush(IThreadPoolWorkItem item)
  58.     {
  59.         int tail = _tailIndex;
  60.         _array[tail] = item;
  61.         _tailIndex = tail + 1;
  62.     }
  63.     // 本线程 Pop(LIFO):从尾部取出
  64.     public bool LocalPop(out IThreadPoolWorkItem? item)
  65.     {
  66.         int tail = _tailIndex - 1;
  67.         if (tail < _headIndex)
  68.         {
  69.             item = null;
  70.             return false;
  71.         }
  72.         _tailIndex = tail;
  73.         item = _array[tail];
  74.         return true;
  75.     }
  76.     // 其他线程窃取(FIFO):从头部取出
  77.     public bool TrySteal(out IThreadPoolWorkItem? item)
  78.     {
  79.         int head = _headIndex;
  80.         if (head >= _tailIndex)
  81.         {
  82.             item = null;
  83.             return false;
  84.         }
  85.         // 使用原子操作避免竞争
  86.         if (Interlocked.CompareExchange(ref _headIndex, head + 1, head) == head)
  87.         {
  88.             item = _array[head];
  89.             return true;
  90.         }
  91.         item = null;
  92.         return false;
  93.     }
  94. }
复制代码
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 倍不均衡使命场景
  1. // 模拟不均衡任务:某些任务耗时长,某些任务耗时短
  2. for (int i = 0; i < 1000; i++)
  3. {
  4.     int taskId = i;
  5.     ThreadPool.QueueUserWorkItem(_ =>
  6.     {
  7.         // 随机耗时:10-1000ms
  8.         Thread.Sleep(Random.Shared.Next(10, 1000));
  9.     });
  10. }
  11. // .NET Framework:
  12. // - 某些线程可能一直执行短任务(快速完成)
  13. // - 某些线程可能一直执行长任务(阻塞很久)
  14. // - 无法自动平衡
  15. // .NET Core(工作窃取):
  16. // - 完成短任务的线程会自动"偷"长任务线程的任务
  17. // - 自动负载均衡
  18. // - 整体完成时间大幅缩短
复制代码
2.3.6 工作盗取的代码示例

完备的模仿实现(简化版):
  1. // 代码示例:WorkStealingDemo.cs
  2. // 注意:这是教学用的简化版,真实实现更复杂
  3. class WorkStealingQueue
  4. {
  5.     private readonly ConcurrentQueue _globalQueue = new();
  6.     private readonly ThreadLocal<Queue> _localQueue = new(() => new Queue());
  7.     // 所有线程的本地队列(用于窃取)
  8.     private static readonly ConcurrentBag<Queue> AllQueues = new();
  9.     public WorkStealingQueue()
  10.     {
  11.         // 注册本地队列
  12.         AllQueues.Add(_localQueue.Value!);
  13.     }
  14.     public void Enqueue(Action action)
  15.     {
  16.         // 优先放入本地队列(LIFO)
  17.         _localQueue.Value!.Enqueue(action);
  18.         // 如果本地队列过大,转移一部分到全局队列
  19.         if (_localQueue.Value.Count > 100)
  20.         {
  21.             for (int i = 0; i < 50; i++)
  22.             {
  23.                 if (_localQueue.Value.TryDequeue(out var item))
  24.                 {
  25.                     _globalQueue.Enqueue(item);
  26.                 }
  27.             }
  28.         }
  29.     }
  30.     public bool TryDequeue(out Action? action)
  31.     {
  32.         // 1. 先尝试从本地队列拿(LIFO)
  33.         if (_localQueue.Value!.Count > 0 && _localQueue.Value.TryDequeue(out action!))
  34.         {
  35.             return true;
  36.         }
  37.         // 2. 本地队列空了,从全局队列拿
  38.         if (_globalQueue.TryDequeue(out action!))
  39.         {
  40.             return true;
  41.         }
  42.         // 3. 全局队列也空了,尝试"偷"其他线程的任务(FIFO)
  43.         foreach (var queue in AllQueues)
  44.         {
  45.             if (queue != _localQueue.Value && queue.Count > 0)
  46.             {
  47.                 lock (queue)  // 简化实现,实际使用无锁算法
  48.                 {
  49.                     if (queue.TryDequeue(out action!))
  50.                     {
  51.                         return true;
  52.                     }
  53.                 }
  54.             }
  55.         }
  56.         action = null;
  57.         return false;
  58.     }
  59. }
  60. // 使用示例
  61. static void DemonstrateWorkStealing()
  62. {
  63.     var queue = new WorkStealingQueue();
  64.     var completed = 0;
  65.     // 提交 1000 个任务
  66.     for (int i = 0; i < 1000; i++)
  67.     {
  68.         int taskId = i;
  69.         queue.Enqueue(() =>
  70.         {
  71.             // 模拟工作
  72.             Thread.Sleep(Random.Shared.Next(1, 10));
  73.             Interlocked.Increment(ref completed);
  74.             Console.WriteLine($"任务 {taskId} 完成,线程 {Environment.CurrentManagedThreadId}");
  75.         });
  76.     }
  77.     // 启动 4 个工作线程
  78.     var workers = new Thread[4];
  79.     for (int i = 0; i < 4; i++)
  80.     {
  81.         workers[i] = new Thread(() =>
  82.         {
  83.             while (completed < 1000)
  84.             {
  85.                 if (queue.TryDequeue(out var action))
  86.                 {
  87.                     action();
  88.                 }
  89.                 else
  90.                 {
  91.                     Thread.Sleep(1);  // 短暂休眠,等待新任务
  92.                 }
  93.             }
  94.         });
  95.         workers[i].Start();
  96.     }
  97.     // 等待所有线程完成
  98.     foreach (var worker in workers)
  99.     {
  100.         worker.Join();
  101.     }
  102.     Console.WriteLine($"所有任务完成!总计:{completed} 个");
  103. }
复制代码
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
  1. // 查看当前配置
  2. ThreadPool.GetMinThreads(out int minWorker, out int minIO);
  3. ThreadPool.GetMaxThreads(out int maxWorker, out int maxIO);
  4. Console.WriteLine($"最小工作线程:{minWorker}");
  5. Console.WriteLine($"最大工作线程:{maxWorker}");
  6. Console.WriteLine($"最小 I/O 线程:{minIO}");
  7. Console.WriteLine($"最大 I/O 线程:{maxIO}");
  8. // 典型输出(8 核 CPU):
  9. // 最小工作线程:8(通常 = CPU 核心数)
  10. // 最大工作线程:32767
  11. // 最小 I/O 线程:8
  12. // 最大 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 徐徐增长到数百毫秒乃至数秒,详细取决于体系负载和吞吐量厘革。

  • 🎯 目标:找到最优线程数目,使吞吐量最大化
  • 📈 战略:徐徐增长线程,监控监控吞吐量厘革
  • 📉 决定

    • 吞吐量上升 → 继续增长线程
    • 吞吐量降落 → 克制增长(过多线程导致上下文切换)
    • 吞吐量持平 → 小幅调解,继续观察

为什么不立刻创建大量线程?
  1. 假设立即创建 100 个线程:
  2.     ↓
  3. ❌ 内存占用:100 MB
  4. ❌ 上下文切换:频繁
  5. ❌ CPU 缓存失效:严重
  6. ❌ 吞吐量下降:可能比 10 个线程还慢
复制代码
Hill Climbing 的智能之处
  1. 逐步增加线程:
  2.     ↓
  3. ✅ 自动找到最优点
  4. ✅ 避免过度创建
  5. ✅ 适应不同工作负载
  6. ✅ 动态调整(任务减少时会减少线程)
复制代码
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线程接纳据件
    1. // 伪代码:ThreadPool 内部逻辑
    2. while (true)
    3. {
    4.     if (TryGetWork(out var workItem))
    5.     {
    6.         // 有任务,执行
    7.         workItem.Execute();
    8.     }
    9.     else
    10.     {
    11.         // 无任务,空闲
    12.         if (WaitForWork(timeout: 20_000))  // 等待 20 秒
    13.         {
    14.             // 有新任务到来,继续工作
    15.             continue;
    16.         }
    17.         else
    18.         {
    19.             // 20 秒后仍无任务
    20.             if (ThreadPool.ThreadCount > ThreadPool.MinThreads)
    21.             {
    22.                 // 线程数超过最小值,可以退出
    23.                 Console.WriteLine($"线程 {Environment.CurrentManagedThreadId} 退出");
    24.                 return;  // 线程终止
    25.             }
    26.             else
    27.             {
    28.                 // 线程数等于最小值,不能退出,继续等待
    29.                 continue;
    30.             }
    31.         }
    32.     }
    33. }
    复制代码
    2.5.4 实行:观察线程池动态调解
    1. // 代码示例:ThreadPoolDynamicDemo.cs
    2. static void DemonstrateThreadPoolDynamic()
    3. {
    4.     ThreadPool.SetMinThreads(2, 2);  // 设置最小线程数为 2
    5.     Console.WriteLine("开始提交任务...");
    6.     for (int i = 0; i < 50; i++)
    7.     {
    8.         int taskId = i;
    9.         ThreadPool.QueueUserWorkItem(_ =>
    10.         {
    11.             Console.WriteLine($"[任务 {taskId}] 线程 {Environment.CurrentManagedThreadId} 开始");
    12.             Thread.Sleep(2000);  // 模拟工作
    13.             Console.WriteLine($"[任务 {taskId}] 完成");
    14.         });
    15.         if (i % 10 == 0)
    16.         {
    17.             ThreadPool.GetAvailableThreads(out int worker, out int io);
    18.             Console.WriteLine($"--- 已提交 {i} 个任务,当前线程池线程数:{ThreadPool.ThreadCount},可用:{worker} ---");
    19.         }
    20.         Thread.Sleep(100);  // 稍微延迟,观察线程注入
    21.     }
    22.     Console.WriteLine("\n等待所有任务完成...");
    23.     Thread.Sleep(10000);
    24.     ThreadPool.GetAvailableThreads(out int finalWorker, out int finalIo);
    25.     Console.WriteLine($"\n最终线程池状态:");
    26.     Console.WriteLine($"  线程数:{ThreadPool.ThreadCount}");
    27.     Console.WriteLine($"  可用工作线程:{finalWorker}");
    28. }
    29. // 预期输出:
    30. // --- 已提交 0 个任务,当前线程池线程数:2,可用:... ---
    31. // [任务 0] 线程 4 开始
    32. // [任务 1] 线程 5 开始
    33. // (等待 500ms,Hill Climbing 决策)
    34. // --- 已提交 10 个任务,当前线程池线程数:3,可用:... ---
    35. // [任务 2] 线程 6 开始
    36. // (等待 500ms)
    37. // --- 已提交 20 个任务,当前线程池线程数:4,可用:... ---
    38. // ...
    39. // (逐步增加线程数)
    40. // --- 已提交 40 个任务,当前线程池线程数:8,可用:... ---
    复制代码
    观察要点

    • 初始阶段:只有 2 个线程(MinThreads)
    • 使命积存:全部线程繁忙,使命列队
    • 徐徐注入:每 500ms 注入 1-2 个新线程
    • 到达稳固:吞吐量不再提升,克制注入
    • 使命完成后:空闲线程在 20 秒后徐徐退出
    2.6 ThreadPool 的性能监控

    2.6.1 及时监控 API
    1. // 获取线程池状态
    2. ThreadPool.GetAvailableThreads(out int availableWorker, out int availableIO);
    3. ThreadPool.GetMinThreads(out int minWorker, out int minIO);
    4. ThreadPool.GetMaxThreads(out int maxWorker, out int maxIO);
    5. // 计算忙碌线程数
    6. int busyWorker = maxWorker - availableWorker;
    7. int busyIO = maxIO - availableIO;
    8. // .NET 5+ 新增:获取挂起任务数
    9. long pendingItems = ThreadPool.PendingWorkItemCount;
    10. // .NET 7+ 新增:获取完成任务数
    11. long completedItems = ThreadPool.CompletedWorkItemCount;
    12. Console.WriteLine($"工作线程:{busyWorker}/{maxWorker} 忙碌");
    13. Console.WriteLine($"I/O 线程:{busyIO}/{maxIO} 忙碌");
    14. Console.WriteLine($"挂起任务:{pendingItems}");
    15. Console.WriteLine($"已完成任务:{completedItems}");
    复制代码
    2.6.2 监控示例
    1. // 代码示例:ThreadPoolMonitorDemo.cs
    2. static void MonitorThreadPool()
    3. {
    4.     var timer = new System.Timers.Timer(1000);
    5.     timer.Elapsed += (s, e) =>
    6.     {
    7.         ThreadPool.GetAvailableThreads(out int worker, out int io);
    8.         ThreadPool.GetMinThreads(out int minWorker, out int minIo);
    9.         ThreadPool.GetMaxThreads(out int maxWorker, out int maxIo);
    10.         int busyWorker = maxWorker - worker;
    11.         long pending = ThreadPool.PendingWorkItemCount;
    12.         Console.WriteLine($"[{DateTime.Now:HH:mm:ss}]");
    13.         Console.WriteLine($"  工作线程:{busyWorker}/{maxWorker} 忙碌,{worker} 可用");
    14.         Console.WriteLine($"  I/O 线程:{maxIo - io}/{maxIo} 忙碌");
    15.         Console.WriteLine($"  挂起任务:{pending}");
    16.         Console.WriteLine($"  当前线程数:{ThreadPool.ThreadCount}");
    17.         Console.WriteLine("---");
    18.     };
    19.     timer.Start();
    20.     // 提交一些任务
    21.     for (int i = 0; i < 100; i++)
    22.     {
    23.         ThreadPool.QueueUserWorkItem(_ => Thread.Sleep(5000));
    24.     }
    25.     Console.WriteLine("按 Enter 停止监控...");
    26.     Console.ReadLine();
    27.     timer.Stop();
    28. }
    29. // 输出示例:
    30. // [14:30:00]
    31. //   工作线程:8/32767 忙碌,32759 可用
    32. //   I/O 线程:0/1000 忙碌
    33. //   挂起任务:92
    34. //   当前线程数:8
    35. // ---
    36. // [14:30:01](500ms 后,Hill Climbing 注入新线程)
    37. //   工作线程:9/32767 忙碌,32758 可用
    38. //   I/O 线程:0/1000 忙碌
    39. //   挂起任务:83
    40. //   当前线程数:9
    41. // ---
    复制代码
    2.7 ThreadPool 的最佳实践

    ✅ 得当利用 ThreadPool 的场景
    1. // 1. 短期任务(< 1 秒)
    2. ThreadPool.QueueUserWorkItem(_ =>
    3. {
    4.     ProcessData();  // 快速完成
    5. });
    6. // 2. CPU 密集型任务(数量可控)
    7. for (int i = 0; i < 100; i++)
    8. {
    9.     ThreadPool.QueueUserWorkItem(_ => ComputePrimes());
    10. }
    11. // 3. 并行计算
    12. Parallel.For(0, 1000, i =>
    13. {
    14.     // Parallel 内部使用 ThreadPool
    15.     ProcessItem(i);
    16. });
    复制代码
    ❌ 不得当利用 ThreadPool 的场景
    1. // 1. 长期运行任务(会耗尽线程池)
    2. ThreadPool.QueueUserWorkItem(_ =>
    3. {
    4.     while (true)  // ❌ 永远运行,占用线程池线程
    5.     {
    6.         Monitor();
    7.         Thread.Sleep(1000);
    8.     }
    9. });
    10. // ✅ 正确做法:使用专用后台线程
    11. var monitorThread = new Thread(() =>
    12. {
    13.     while (true)
    14.     {
    15.         Monitor();
    16.         Thread.Sleep(1000);
    17.     }
    18. })
    19. {
    20.     IsBackground = true
    21. };
    22. monitorThread.Start();
    23. // 2. I/O 密集型任务(应使用 async/await)
    24. ThreadPool.QueueUserWorkItem(_ =>
    25. {
    26.     var data = httpClient.GetStringAsync(url).Result;  // ❌ 阻塞线程
    27. });
    28. // ✅ 正确做法:使用异步
    29. await httpClient.GetStringAsync(url);  // 不阻塞线程
    30. // 3. 需要精确控制的任务
    31. ThreadPool.QueueUserWorkItem(_ =>
    32. {
    33.     // ❌ 无法设置线程优先级
    34.     // ❌ 无法设置线程名称
    35.     // ❌ 无法控制线程生命周期
    36. });
    37. // ✅ 正确做法:使用专用线程
    38. var thread = new Thread(() => { /* work */ })
    39. {
    40.     Priority = ThreadPriority.High,
    41.     Name = "Worker-1"
    42. };
    43. thread.Start();
    复制代码
    2.8 性能对比:.NET Framework vs .NET Core

    2.8.1 微基准测试
    1. // 代码示例:需要分别在两个项目中运行
    2. // 项目 1:Threads(.NET 10)
    3. // 项目 2:ThreadsFramework(.NET Framework 4.8)
    4. static void BenchmarkThreadPool()
    5. {
    6.     const int taskCount = 100_000;
    7.     var sw = Stopwatch.StartNew();
    8.     var countdown = new CountdownEvent(taskCount);
    9.     for (int i = 0; i < taskCount; i++)
    10.     {
    11.         ThreadPool.QueueUserWorkItem(_ =>
    12.         {
    13.             // 简单工作
    14.             var result = Math.Sqrt(42);
    15.             countdown.Signal();
    16.         });
    17.     }
    18.     countdown.Wait();
    19.     sw.Stop();
    20.     Console.WriteLine($"完成 {taskCount:N0} 个任务:{sw.ElapsedMilliseconds} ms");
    21.     Console.WriteLine($"吞吐量:{taskCount * 1000.0 / sw.ElapsedMilliseconds:N0} tasks/s");
    22. }
    23. // 实际测试结果(8 核 CPU):
    24. // .NET Framework 4.8:
    25. //   完成 100,000 个任务:2,000 ms
    26. //   吞吐量:50,000 tasks/s
    27. //
    28. // .NET Core / .NET 10:
    29. //   完成 100,000 个任务:500 ms
    30. //   吞吐量:200,000 tasks/s
    31. //
    32. // 性能提升: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 是异步利用的抽象
    1. Task = Promise/Future 模式的实现
    2. Promise:承诺将来会有一个结果
    3. Future:未来的结果
    4. Task:
    5.   - 可能已经完成
    6.   - 可能正在执行
    7.   - 可能还未开始
    8.   - 可能失败(异常)
    9.   - 可能被取消
    复制代码
    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代码示例
    1. // 代码示例:TaskStateDemo.cs
    2. static void DemonstrateTaskStates()
    3. {
    4.     // Created
    5.     var task = new Task(() => Thread.Sleep(1000));
    6.     Console.WriteLine($"状态:{task.Status}");  // Created
    7.     // WaitingForActivation
    8.     task.Start();
    9.     Console.WriteLine($"状态:{task.Status}");  // WaitingToRun / Running
    10.     // Running
    11.     Thread.Sleep(100);
    12.     Console.WriteLine($"状态:{task.Status}");  // Running
    13.     // RanToCompletion
    14.     task.Wait();
    15.     Console.WriteLine($"状态:{task.Status}");  // RanToCompletion
    16. }
    复制代码
    3.2 Task 与 ThreadPool 的协作

    Task.Run 的本质
    1. // 源码简化版(CoreCLR)
    2. public static Task Run(Action action)
    3. {
    4.     return Task.InternalStartNew(
    5.         null,
    6.         action,
    7.         null,
    8.         CancellationToken.None,
    9.         TaskScheduler.Default,  // ← 关键:使用 ThreadPoolTaskScheduler
    10.         TaskCreationOptions.DenyChildAttach,
    11.         InternalTaskOptions.None
    12.     );
    13. }
    复制代码
    完备流程可视化
    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 的内存开销
    1. // Task 对象的大致结构(简化)
    2. class Task
    3. {
    4.     private int _stateFlags;           // 4 bytes
    5.     private object _action;            // 8 bytes(64 位)
    6.     private object _result;            // 8 bytes
    7.     private TaskScheduler _scheduler;  // 8 bytes
    8.     private volatile int _id;          // 4 bytes
    9.     private CancellationToken _token;  // ~16 bytes
    10.     // ... 其他字段
    11.    
    12.     // 总计:约 100-200 bytes
    13. }
    复制代码
    对比
    1. 1 个 Thread:约 1 MB
    2. 1 个 Task:约 100-200 bytes
    3. 10000 个 Thread:约 10 GB
    4. 10000 个 Task:约 1-2 MB(内存节省 5000 倍!)
    复制代码
    3.3 I/O 麋集型 Task 不占用线程

    焦点区别
    1. // CPU 密集型 Task(占用线程)
    2. var task1 = Task.Run(() =>
    3. {
    4.     for (int i = 0; i < 1000000; i++)
    5.     {
    6.         var result = Math.Sqrt(i);  // CPU 计算
    7.     }
    8. });
    9. // I/O 密集型 Task(不占用线程)
    10. var task2 = Task.Run(async () =>
    11. {
    12.     await Task.Delay(1000);  // ← 这里不占用线程!
    13. });
    复制代码
    底层机制可视化
    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 体系的事故关照内存对比
    1. 10000 个 CPU 密集型 Task:
    2.   - 需要约 100 个线程(假设长时间运行)
    3.   - 内存:100 × 1 MB = 100 MB(线程栈)
    4.   - 任务对象:10000 × 200 bytes = 2 MB
    5.   - 总计:约 102 MB
    6. 10000 个 I/O 密集型 Task:
    7.   - 需要约 10-20 个线程(快速释放)
    8.   - 内存:20 × 1 MB = 20 MB(线程栈)
    9.   - 任务对象:10000 × 200 bytes = 2 MB
    10.   - 总计:约 22 MB
    11. 内存节省:80 MB(约 78%)
    复制代码
    实行:验证 I/O 利用不占用线程
    1. // 代码示例:IoTaskThreadDemo.cs
    2. static async Task DemonstrateIoTaskThreadUsage()
    3. {
    4.     Console.WriteLine($"初始线程数:{ThreadPool.ThreadCount}");
    5.     // 创建 10000 个 I/O 密集型 Task
    6.     var tasks = new Task[10000];
    7.     for (int i = 0; i < 10000; i++)
    8.     {
    9.         tasks[i] = Task.Run(async () =>
    10.         {
    11.             await Task.Delay(5000);  // I/O 操作
    12.         });
    13.     }
    14.     await Task.Delay(100);  // 等待任务启动
    15.     Console.WriteLine($"10000 个 I/O Task 运行中,线程数:{ThreadPool.ThreadCount}");
    16.     // 预期输出:约 10-20 个(而不是 10000 个!)
    17.     await Task.WhenAll(tasks);
    18. }
    复制代码
    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默认调治器
    1. // TaskScheduler.Default = ThreadPoolTaskScheduler
    2. Task.Run(() => Console.WriteLine("使用默认调度器"));
    3. // 等价于
    4. Task.Factory.StartNew(
    5.     () => Console.WriteLine("使用默认调度器"),
    6.     CancellationToken.None,
    7.     TaskCreationOptions.None,
    8.     TaskScheduler.Default  // ← ThreadPoolTaskScheduler
    9. );
    复制代码
    调治流程可视化
    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:限定并发数的调治器
    1. // 示例:限制并发数的调度器
    2. var scheduler = new LimitedConcurrencyLevelTaskScheduler(maxDegreeOfParallelism: 4);
    3. for (int i = 0; i < 100; i++)
    4. {
    5.     Task.Factory.StartNew(
    6.         () => { /* 工作 */ },
    7.         CancellationToken.None,
    8.         TaskCreationOptions.None,
    9.         scheduler  // 最多 4 个任务并发执行
    10.     );
    11. }
    复制代码
    自界说调治器内部机制
    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 线程调治器
    1. // WPF/WinForms:确保在 UI 线程更新界面
    2. private async void Button_Click(object sender, EventArgs e)
    3. {
    4.     // 后台任务
    5.     var result = await Task.Run(() =>
    6.     {
    7.         // 在 ThreadPool 线程执行(非 UI 线程)
    8.         return ExpensiveComputation();
    9.     });
    10.     // 自动回到 UI 线程
    11.     // await 后的代码在原 SynchronizationContext 执行
    12.     this.TextBox.Text = result;  // ✅ 安全:在 UI 线程
    13. }
    14. // 或者显式指定 UI 调度器
    15. await Task.Run(() => Work())
    16.     .ContinueWith(
    17.         t => UpdateUI(t.Result),
    18.         TaskScheduler.FromCurrentSynchronizationContext()  // UI 线程
    19.     );
    复制代码
    关键洞察

    • 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 资源线程归还池,等待复用扩展性差(线程数目有限)✅ 好(复用机制)焦点区别
    1. new Thread():
    2.   1. 每次都创建新的 OS 线程
    3.   2. 消耗 1-8 MB 内存
    4.   3. 执行完成后销毁线程
    5.   4. 无法复用
    6. Task.Run():
    7.   1. 优先从线程池复用线程
    8.   2. 只在必要时创建新线程
    9.   3. 执行完成后线程归还池中
    10.   4. 高效复用
    复制代码
    4.3 Task 的两种实行路径

    路径 1:CPU 麋集型(利用 ThreadPool 线程)
    1. 用户代码
    2.     ↓
    3. Task.Run(() => ComputePrimes())
    4.     ↓
    5. 进入 ThreadPool 工作队列
    6.     ↓
    7. ThreadPool 线程执行 lambda
    8.     ↓
    9. 占用线程,直到计算完成
    复制代码
    路径 2:I/O 麋集型(不占用线程)
    1. 用户代码
    2.     ↓
    3. await httpClient.GetAsync(url)
    4.     ↓
    5. 发起异步 I/O 请求(OS 级别)
    6.     ↓
    7. 线程立即释放(返回线程池)
    8.     ↓
    9. (等待网络响应,不占用线程)
    10.     ↓
    11. I/O 完成端口(IOCP)收到响应
    12.     ↓
    13. ThreadPool I/O 线程处理回调
    14.     ↓
    15. 从线程池取一个线程继续执行后续代码
    复制代码
    4.4 完备流程图:从 async/await 到 Thread
    1. 用户代码:
    2. async Task ProcessAsync()
    3. {
    4.     await Task.Run(() => ComputePrimes());
    5. }
    6. 流程:
    7. 1. 编译器生成状态机(AsyncMethodBuilder)
    8. 2. await Task.Run(...) 创建 Task 对象
    9. 3. Task.Run 调用 TaskScheduler.Default.QueueTask
    10. 4. ThreadPoolTaskScheduler 调用 ThreadPool.QueueUserWorkItem
    11. 5. ThreadPool 将任务放入工作队列
    12. 6. 某个线程池线程(Thread)从队列取出任务
    13. 7. 线程执行 ComputePrimes()
    14. 8. 完成后,Task 状态变为 RanToCompletion
    15. 9. 状态机恢复执行后续代码
    复制代码
    📊 实战对比

    实战 1:创建 10000 个 Thread vs ThreadPool
    1. // 代码示例:ThreadVsThreadPoolDemo.cs
    2. // 见文章开头的实验
    复制代码
    实战 2:ThreadPool 监控
    1. // 代码示例:ThreadPoolMonitorDemo.cs
    2. static void MonitorThreadPool()
    3. {
    4.     var timer = new System.Timers.Timer(1000);
    5.     timer.Elapsed += (s, e) =>
    6.     {
    7.         ThreadPool.GetAvailableThreads(out int worker, out int io);
    8.         ThreadPool.GetMinThreads(out int minWorker, out int minIo);
    9.         ThreadPool.GetMaxThreads(out int maxWorker, out int maxIo);
    10.         
    11.         Console.WriteLine($"可用工作线程:{worker}/{maxWorker}");
    12.         Console.WriteLine($"可用 I/O 线程:{io}/{maxIo}");
    13.         Console.WriteLine($"当前线程数:{ThreadPool.ThreadCount}");
    14.         Console.WriteLine("---");
    15.     };
    16.     timer.Start();
    17.    
    18.     // 提交一些任务
    19.     for (int i = 0; i < 100; i++)
    20.     {
    21.         ThreadPool.QueueUserWorkItem(_ => Thread.Sleep(5000));
    22.     }
    23.    
    24.     Console.ReadLine();
    25. }
    复制代码
    实战 3:.NET Framework vs .NET Core 性能对比
    1. // 需要分别在两个项目中运行
    2. // 项目 1:Threads(.NET 10)
    3. // 项目 2:ThreadsFramework(.NET Framework 4.8)
    4. static void BenchmarkThreadPool()
    5. {
    6.     const int taskCount = 100000;
    7.     var sw = Stopwatch.StartNew();
    8.    
    9.     var countdown = new CountdownEvent(taskCount);
    10.     for (int i = 0; i < taskCount; i++)
    11.     {
    12.         ThreadPool.QueueUserWorkItem(_ =>
    13.         {
    14.             // 简单工作
    15.             var result = Math.Sqrt(42);
    16.             countdown.Signal();
    17.         });
    18.     }
    19.    
    20.     countdown.Wait();
    21.     sw.Stop();
    22.    
    23.     Console.WriteLine($"完成 {taskCount} 个任务:{sw.ElapsedMilliseconds}ms");
    24. }
    25. // 预期结果:
    26. // .NET Framework 4.8:约 2000ms
    27. // .NET Core / .NET 10:约 500ms(快 4 倍!)
    复制代码
    🔥 常见误区澄清

    误区 1:"Task 就是线程"

    错误认知
    1. var task = Task.Run(() => DoWork());
    2. // 认为:创建了一个新线程
    复制代码
    原形
    1. // Task = 异步操作的抽象
    2. // 可能使用 ThreadPool 线程(CPU 密集型)
    3. // 可能不占用线程(I/O 密集型)
    4. // CPU 密集型:使用线程池线程
    5. var task1 = Task.Run(() => ComputePrimes());
    6. // I/O 密集型:不占用线程
    7. var task2 = Task.Run(async () => await httpClient.GetAsync(url));
    复制代码
    误区 2:"Task.Run 会创建新线程"

    错误认知
    1. for (int i = 0; i < 10000; i++)
    2. {
    3.     Task.Run(() => DoWork());
    4. }
    5. // 认为:创建了 10000 个线程
    复制代码
    原形
    1. // Task.Run 只是将任务放入 ThreadPool 队列
    2. // 实际使用的线程数 = ThreadPool.ThreadCount(约 10-100 个)
    3. // 10000 个 Task 会排队等待执行
    复制代码
    误区 3:"async 会创建新线程"

    错误认知
    1. async Task ProcessAsync()
    2. {
    3.     await Task.Delay(1000);
    4. }
    5. // 认为:async 关键字会创建线程
    复制代码
    原形
    1. // async 只是语法糖,生成状态机
    2. // 不会创建线程
    3. // await Task.Delay 使用 OS 定时器,不占用线程
    复制代码
    🎯 最佳实践发起

    1. 优先利用 Task 而不是 Thread
    1. // ❌ 不推荐
    2. new Thread(() => DoWork()).Start();
    3. // ✅ 推荐
    4. Task.Run(() => DoWork());
    复制代码
    2. I/O 麋集型利用 async/await
    1. // ❌ 不推荐(阻塞线程)
    2. var data = httpClient.GetStringAsync(url).Result;
    3. // ✅ 推荐(不阻塞线程)
    4. var data = await httpClient.GetStringAsync(url);
    复制代码
    3. 不要耗尽 ThreadPool
    1. // ❌ 不推荐(耗尽线程池)
    2. for (int i = 0; i < 10000; i++)
    3. {
    4.     Task.Run(() => Thread.Sleep(10000));  // 长期占用
    5. }
    6. // ✅ 推荐(使用 SemaphoreSlim 限流)
    7. var semaphore = new SemaphoreSlim(10);  // 最多 10 个并发
    8. for (int i = 0; i < 10000; i++)
    9. {
    10.     await semaphore.WaitAsync();
    11.     Task.Run(async () =>
    12.     {
    13.         try
    14.         {
    15.             await DoWorkAsync();
    16.         }
    17.         finally
    18.         {
    19.             semaphore.Release();
    20.         }
    21.     });
    22. }
    复制代码
    4. 监控 ThreadPool 康健度
    1. // 定期检查
    2. ThreadPool.GetAvailableThreads(out int worker, out int io);
    3. if (worker < 10)
    4. {
    5.     Console.WriteLine("警告:线程池即将耗尽!");
    6. }
    复制代码
    📚 总结

    焦点要点


    • Thread

      • OS 级别的实行单位
      • 创建/烧毁资源高(约 1 MB 内存)
      • 上下文切换资源高
      • 当代开发中很少直接利用

    • ThreadPool

      • 线程复用机制
      • .NET Core 重构:工作盗取算法,性能提升明显
      • 动态调解线程数(Hill Climbing)
      • Task 的底层底子

    • Task

      • 异步利用的抽象
      • CPU 麋集型:利用 ThreadPool 线程
      • I/O 麋集型:不占用线程
      • 当代 .NET 并发编程的首选

    三者关系
    1. Thread(基础)
    2.     ↓
    3. ThreadPool(优化)
    4.     ↓
    5. Task(抽象)
    6.     ↓
    7. 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企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金.
  • 回复

    使用道具 举报

    登录后关闭弹窗

    登录参与点评抽奖  加入IT实名职场社区
    去登录
    快速回复 返回顶部 返回列表