📚 **Day 11 · Phase 2 第 4 课**
6推送消息数
16826字符数
2026-08-10教学日期
<!-- message_id: om_x100b68a51d95b4a0b27a5a3acf57e20 -->
📚 **Day 11 · Phase 2 第 4 课**
**主题**:CLR 线程池与 TaskScheduler(ThreadPool / Work-Stealing / Hill Climbing / TaskScheduler / LongRunning / WPF UI 调度)
一、今日知识点
🎯 是什么
CLR **线程池**(System.Threading.ThreadPool)是 .NET **进程级共享**的工作线程池,由 CLR 在进程启动时创建并维护。它解决两个根本问题:
1. **避免反复创建/销毁线程**(线程创建 ~100 μs + 内核对象分配 + 栈内存分配 ~1 MB,销毁时还要走 GC + 析构)
2. **自适应并发控制**:根据 CPU 核心数、工作负载类型(CPU-bound / I/O-bound)动态注入/回收线程
TaskScheduler(System.Threading.Tasks.TaskScheduler)是 Task 的**调度策略抽象**,决定一个 Task「**何时、在哪个线程、用什么队列**」运行。默认调度器(ThreadPoolTaskScheduler)把 Task 扔进线程池全局队列;你可以自定义调度器(例如 SynchronizationContextTaskScheduler 把 Task 调度到捕获的 UI 线程)。
🤔 为什么
线程池的存在不是"性能优化",是**架构必然性**:
- **线程创建是重操作**:Windows 内核对象 + 用户态栈 (~1 MB) + 描述符 + 句柄表项 = ~100 μs
- **线程销毁走完整 GC 路径**:FinalizerThread 串行处理 + 句柄关闭 + 栈回收
- **CPU 缓存不友好**:每条新线程的 L1/L2 都是冷的,跨核时连 L3 都丢
- **上下文切换**:Windows ~1-10 μs,Linux ~2-5 μs,每次切换约等于几百到几千条指令
所以线程池的本质 = **「工作线程复用 + 自适应并发 + 最小化系统调用」**。Task 调度策略抽象(TaskScheduler)则是把"线程池"从"线程执行"中抽离出来,让你能用同一个 Task API 但调度到不同上下文(线程池 / UI 线程 / 自定义调度器 / 调试可视化)。
🛠️ 怎么用(6 种核心入口)
// 1. 最原始:QueueUserWorkItem(无 Task 包装,无返回值)
ThreadPool.QueueUserWorkItem(state => {
Console.WriteLine($"Worker thread: {Thread.CurrentThread.ManagedThreadId}");
}, null);
// 2. 推荐入口:Task.Run(自动管理 Task 引用、自动 Unwrap 嵌套)
Task.Run(() => {
// 后台工作
return ComputeXxx(); // 返回 Task<T> 会自动 Unwrap
});
// 3. 更多控制:Task.Factory.StartNew
// 可以指定 TaskScheduler、TaskCreationOptions
var task = Task.Factory.StartNew(() => {
// 长耗时操作
}, CancellationToken.None,
TaskCreationOptions.LongRunning, // 👈 关键:独立线程,不入池
TaskScheduler.Default);
// 4. UI 续作:捕获 SynchronizationContext(TaskScheduler.FromCurrentSynchronizationContext)
// WPF 场景:在 ViewModel 里 await → 自动回到 UI 线程(捕获的 DispatcherSynchronizationContext)
await Task.Run(() => HeavyCompute()).ConfigureAwait(false); // 后台
UpdateUi(); // 自动回到 UI 线程(因 UI 线程有 SynchronizationContext)
// 5. 自定义调度器:例如并发度受限的调度器
var limitedScheduler = new ConcurrentExclusiveSchedulerPair(
maxConcurrencyLevel: 4).ConcurrentScheduler;
Task.Factory.StartNew(Work, CancellationToken.None,
TaskCreationOptions.None, limitedScheduler);
// 6. 显式调度到 UI 线程(不依赖 SynchronizationContext 捕获)
TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task.Run(() => ComputeOnBackground())
.ContinueWith(t => UpdateUi(t.Result),
CancellationToken.None,
TaskContinuationOptions.OnlyOnRanToCompletion,
uiScheduler);
⚠️ 常见误区(6 个最致命的反模式)
1. **Task.Run(...).Result / .Wait() 在 UI 线程 = 死锁**
经典 .NET Framework 死锁:UI 线程 Wait() → 阻塞 → 续作无法回到 UI 线程 → 永远等。**WPF 中必须 await 而不是 .Wait()**。
2. **「异步包装同步」用 Task.Run 浪费线程池**// ❌ 错:把同步 I/O 扔进线程池(占 worker thread,I/O 是 IOCP 完成的)
public Task<string> ReadAsync() => Task.Run(() => File.ReadAllText(path));
// ✅ 对:真异步 API(Socket.ReadAsync / Stream.ReadAsync / HttpClient.SendAsync)
public Task<string> ReadAsync() => File.ReadAllTextAsync(path);
3. **大量并发 I/O 用 ThreadPool.SetMinThreads(100, 100) 错误**
ThreadPool 区分 **worker thread**(CPU 工作)和 **IOCP thread**(I/O 完成端口)。I/O 完成走 IOCP,**不占 worker thread**。盲目调大 min threads 会让 worker thread 数量膨胀 → 调度开销 + GC 压力 + L3 抖动。
4. **TaskCreationOptions.LongRunning 不是"加速"**
它是「**给线程池的暗示:这是个长任务,别指望快速复用**」。CLR 会**为这个 Task 创建一个独立线程**(不入池),释放线程池容量。但滥用 = 等同于手写 new Thread(...),反而毁掉线程池复用。
5. **Task.Delay(1000).Wait() ≠ Thread.Sleep(1000)**
前者**占用一个池线程**在等定时器(直到 1s 后才释放),如果循环 1000 次 = 1000 个池线程被占 → 饥饿。后者是**让出当前线程**,不占池线程。
6. **Task.Run 内捕获 SynchronizationContext**
续作会自动回到捕获的上下文(UI 线程),但如果捕获到的是 UI 线程 + 后面有同步阻塞 → 死锁。**WPF 视图层异步代码必须 ConfigureAwait(false) 或纯 await 链**。
二、关联知识
- **Day 4(async/await)**:await 续作调度由 TaskScheduler 决定,WPF 中默认捕获 DispatcherSynchronizationContext 自动回 UI 线程
- **Day 8(GC)**:worker thread 是 GC 根(StackRoot),线程局部变量(栈上引用)阻止对象回收
- **Day 9(JIT)**:R2R + Tiered Compilation 对 worker thread 的影响相同,但**线程越多 JIT 缓存压力越大**(每个线程的 Tier 1/2 独立)
- **Day 10(异常)**:worker thread 异常默认不传播(.NET Core 2+ 起 → 终止进程;早期版本被「吞掉」)
- **下次起点(Day 12)**:锁机制(Monitor / SemaphoreSlim / AsyncLock)—— 线程池之上的并发原语
**第 1 段结束。下条发 Q1 面试题 + Q1 复盘。**
---
<!-- message_id: om_x100b68a51d62d8a0b3c28ac4813b1a4 -->
二、3 道递进式面试题
Q1(基础概念)
请简述 **CLR 线程池** 的设计哲学和 **TaskScheduler** 的作用。为什么 .NET 要默认把 Task 调度到线程池而不是为每个 Task 创建新线程?请从「线程创建成本、CPU 缓存、GC 压力、调度开销」四个维度分析。
✅ Q1 答案要点
**线程池设计哲学**:
1. **线程复用**——避免重复创建/销毁线程(~100 μs + 1MB 栈 + 句柄)
2. **自适应并发**——根据 CPU 核心数 + 工作负载动态注入/回收线程(min=CPU 核心数, max=32,767)
3. **工作窃取(Work-Stealing)**——本地队列 + 全局队列,每个 worker thread 优先消费自己的本地队列,空闲时从其他线程的本地队列尾部「窃取」
4. **注入控制**——早期是「Hill Climbing」爬山算法,.NET Core 3+ 改为「Starvation Avoidance」避免饥饿
**TaskScheduler 作用**:
- **抽象「Task 调度策略」**——把"线程池"从"线程执行"中抽离出来
- **支持自定义调度**——UI 线程(SynchronizationContextTaskScheduler)/ 并发受限(ConcurrentExclusiveSchedulerPair)/ 调试可视化(自定义 TraceScheduler)
- **让 Task API 与线程池解耦**——同一个 Task.Run 可以跑在不同上下文
**为什么不每个 Task 创建新线程**(4 维度):
1. **线程创建成本**:~100 μs + 1 MB 栈 + 内核对象 + 句柄表项 = 系统调用 + 内核态切换
2. **CPU 缓存**:新线程的 L1/L2 是冷的,跨核时连 L3 都丢;线程复用保证热路径 CPU 缓存命中
3. **GC 压力**:每个线程有独立栈(GC 根),线程越多 → 扫描根的成本越高
4. **调度开销**:Windows 内核态线程调度器 + 上下文切换 ~1-10 μs/次
**易错点**:很多开发者以为"线程池就是性能优化"——错。线程池是**架构必然性**,没有线程池的并发系统根本无法承受高并发场景。
**第 2 段结束。下条发 Q2 面试题 + Q2 复盘。**
---
<!-- message_id: om_x100b68a51aff74a4b16ea75fa63b761 -->
Q2(原理与辨析)
CLR ThreadPool 的 **工作窃取算法(Work-Stealing)** 是如何工作的?**Hill Climbing** 和 **Starvation Avoidance** 算法在什么场景下被使用?TaskCreationOptions.LongRunning 在底层做了什么?为什么说「线程池不是为长时间运行的任务设计的」?请给出具体的注入阈值/算法伪代码/数据范围。
✅ Q2 答案要点
工作窃取(Work-Stealing)详解
**数据结构**(每个 worker thread 都有自己的):
- **本地队列**(Local Queue):环形数组,默认长度 16(MPSC,Multi-Producer Single-Consumer)
- **全局队列**(Global Queue):FIFO 链表
- **偷取队列**:其他 worker thread 偷取时的目标
**算法流程**:
Worker thread 循环:
1. 优先消费本地队列(LIFO,自己刚 push 的热数据,缓存友好)
2. 本地空 → 偷取其他线程的本地队列(FIFO,对方的旧数据,缓存影响小)
3. 都空 → 全局队列
4. 都空 → 等待事件(注入新任务 / 偷取成功 / 退出条件)
**为什么这样设计**:
- **本地 LIFO**:自己刚 push 的任务大概率访问同一份数据 → CPU 缓存命中
- **偷取 FIFO**:对方线程的"旧数据"对方已处理完,偷过来处理时对方的 L1/L2 早已被换掉,**偷 FIFO 避免对方已经预热的缓存行被偷走**
Hill Climbing(爬山算法)—— 早期 .NET Framework
**核心思想**:每 N 秒采样线程池吞吐(completed tasks/sec),动态调整 worker thread 数量。
**伪代码**:
loop:
throughput_now = sample_throughput()
if throughput_now > throughput_prev:
thread_count += step_up // 加线程
else:
thread_count -= step_down // 减线程
if starved(global_queue):
inject_min_thread() // 立即注入
sleep(sample_interval) // 通常 1-10 秒
**缺陷**:对突发流量响应慢(要等下一个采样周期)、震荡(过调)、对长尾任务不友好。
Starvation Avoidance(饥饿避免)—— .NET Core 3+ 当前实现
**核心思想**:**主动监控 worker 饥饿时间**,达到阈值立即注入新线程。
**关键参数**(.NET 6+):
- **饥饿阈值**(Starvation Time):worker thread 在全局队列里等待 > ~500 ms 未被消费
- **注入速率限制**:避免短时间内疯狂注入(洪泛保护)
**伪代码**:
worker_thread_loop:
while running:
task = pop_local() or steal() or pop_global()
if task is null:
starvation_start = now()
wait_for_signal() // 等待新任务
if now() - starvation_start > starvation_threshold:
inject_new_worker_thread() // 主动注入
else:
execute(task)
**优势**:对突发流量响应快(不需要采样)、无震荡、对长尾任务友好。
TaskCreationOptions.LongRunning 底层做了什么
// 简化的内部实现
Task.Factory.StartNew(action, options: LongRunning) {
if (options.HasFlag(LongRunning)) {
// 👇 关键:使用 Thread 池外的独立线程,不进线程池
var thread = new Thread(s => ((Action)s)());
thread.IsBackground = true;
thread.Start(action);
return Task.Factory.FromAsync(thread, ...); // 包装为 Task
}
// 否则调度到 ThreadPool
}
**为什么不入池**:CLR 认为这个任务「会运行很久」(通常 > 500 ms),如果让它占着池线程,会减少池的吞吐。**独立线程释放池容量给短任务**。
为什么线程池不适合长任务(3 个理由)
1. **饥饿风险**:worker thread 被长任务占满 → 短任务排队 → 整个进程响应变慢
2. **注入失控**:线程池会因长任务注入越来越多线程 → 上下文切换成本失控 → 性能反降
3. **GC 根膨胀**:每个 worker thread 都是 GC 根,长任务线程积累 → GC 扫描成本高
**判定标准**:任务 > 500 ms 或不确定持续时间 → 用 LongRunning 或异步 I/O API。
**第 3 段结束。下条发 Q3 实战面试题。**
---
<!-- message_id: om_x100b68a51aab7ca0b22bd23247a6dbc -->
Q3(实战与深度)
在一个 **WPF 实时数据看板** 中,需要**并行调用 5 个独立的微服务**(每个响应 50-500 ms),并实时更新 UI 进度。请设计完整的并发架构,要求:
- **(a)** 不阻塞 UI 线程
- **(b)** 单个服务失败不影响其他服务(隔离)
- **(c)** 进度反馈 60 fps(流畅)
- **(d)** 支持取消(用户随时可停止)
请说明:
1. 每层使用什么 TaskScheduler?
2. 如何控制并发度(5 个服务全并发还是限流)?
3. 如何避免常见陷阱(线程池饥饿 / UI 线程死锁 / 异常吞没)?
4. 给出完整的可运行代码骨架(5 个服务调用 + 进度更新 + 取消 + 异常隔离)
**第 4 段结束。下条发 Q3 复盘 + 完整架构方案 + 代码。**
---
<!-- message_id: om_x100b68a51bead8acb1d4ff2181f9df4 -->
───
三、全面复盘讲解(Q3)
🏗️ 5 层架构方案
┌──────────────────────────────────────────────┐
│ Layer 1: UI 层(WPF ViewModel) │
│ - DispatcherSynchronizationContext 捕获 │
│ - IProgress<T> 自动回到 UI 线程 │
│ - CancellationTokenSource 触发取消 │
├──────────────────────────────────────────────┤
│ Layer 2: 调度层(并发控制) │
│ - Task.WhenAll + 独立 Task 包装 │
│ - SemaphoreSlim(5) 限流(或全并发,5 个数)│
│ - ConcurrentExclusiveSchedulerPair │
├──────────────────────────────────────────────┤
│ Layer 3: 业务层(5 个微服务调用) │
│ - Task.Run 调度到 ThreadPool │
│ - 每个 Task 独立 try/catch(异常隔离) │
│ - HttpClient.SendAsync 真异步 I/O │
├──────────────────────────────────────────────┤
│ Layer 4: 进度聚合层 │
│ - Channel<ProgressReport> 背压解耦 │
│ - Dispatcher 节流到 60 fps │
│ - CompositionTarget.Rendering 帧对齐 │
├──────────────────────────────────────────────┤
│ Layer 5: 遥测/告警层 │
│ - 后台线程消费异常 │
│ - 失败任务统一上报 │
│ - 自动重试策略(Transient Fault Handling) │
└──────────────────────────────────────────────┘
💻 完整代码骨架
using System.Net.Http;
using System.Threading.Channels;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
public partial class DashboardViewModel : ObservableObject {
private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(3) };
private CancellationTokenSource? _cts;
[ObservableProperty] private double _progress; // 0-100
[ObservableProperty] private string _statusText = "就绪";
// 5 个微服务调用
private static readonly (string Name, string Url)[] Services = {
("用户服务", "https://api.example.com/users"),
("订单服务", "https://api.example.com/orders"),
("库存服务", "https://api.example.com/inventory"),
("日志服务", "https://api.example.com/logs"),
("统计服务", "https://api.example.com/stats"),
};
public async Task LoadAllAsync() {
// 🎯 Layer 1: 取消旧任务 + 重置状态
_cts?.Cancel();
_cts = new CancellationTokenSource();
var ct = _cts.Token;
Progress = 0;
StatusText = "加载中...";
// 🎯 Layer 4: Channel 背压 + IProgress 自动回 UI 线程
var progress = new Progress<int>(p => Progress = p);
var channel = Channel.CreateUnbounded<(string name, bool ok, Exception? err)>();
// 🎯 Layer 2: 并发控制(5 个全并发,SemaphoreSlim 也可)
// 由于只有 5 个,全并发即可;如果以后扩展到 50+ 个,用 SemaphoreSlim(8) 限流
var tasks = Services.Select(service =>
CallServiceAsync(service.Name, service.Url, progress, channel.Writer, ct)
).ToArray();
// 🎯 启动后台消费者处理失败上报(避免阻塞业务)
var consumerTask = ConsumeFailuresAsync(channel.Reader);
// 🎯 Layer 3: 全部完成或任意失败(继续等待其他任务)
await Task.WhenAll(tasks).ConfigureAwait(false);
channel.Writer.Complete();
await consumerTask;
Progress = 100;
StatusText = "完成";
// 异常处理:聚合所有失败
var failures = tasks.Where(t => t.IsFaulted).ToList();
if (failures.Any()) {
StatusText = $"完成,但 {failures.Count} 项失败";
}
}
// 🎯 Layer 3: 单个服务调用(异常隔离)
private async Task CallServiceAsync(
string name, string url,
IProgress<int> progress,
ChannelWriter<(string, bool, Exception?)> failureWriter,
CancellationToken ct) {
try {
// 🎯 关键:HttpClient.SendAsync 是真异步(IOCP),不占 worker thread
var sw = Stopwatch.StartNew();
var response = await _http.GetAsync(url, ct).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
// 模拟业务处理(CPU 工作,扔进线程池)
var data = await Task.Run(() => ProcessResponse(
response.Content), ct).ConfigureAwait(false);
sw.Stop();
progress.Report((int)((1.0 / Services.Length) * 100));
} catch (OperationCanceledException) {
throw; // 取消要传播
} catch (Exception ex) {
// 🎯 关键:失败上报但不抛(隔离)
await failureWriter.WriteAsync((name, false, ex), ct);
progress.Report((int)((1.0 / Services.Length) * 100));
}
}
// 🎯 Layer 5: 后台消费者(失败统一上报)
private async Task ConsumeFailuresAsync(
ChannelReader<(string, bool, Exception?)> reader) {
await foreach (var (name, ok, err) in reader.ReadAllAsync()) {
if (!ok) {
// 🎯 关键:Dispatch 失败到 UI 线程显示告警
await Application.Current.Dispatcher.InvokeAsync(() => {
MessageBox.Show($"{name} 调用失败:
{err?.Message}", "告警");
});
}
}
}
public void Cancel() {
_cts?.Cancel(); // 触发所有任务的 ct
}
}
🎯 各层 TaskScheduler 选择
| 层 | TaskScheduler | 理由 |
| ----- | ------------------------------------- | ---------------------- |
| UI 层 | DispatcherSynchronizationContext (隐式) | await 自动捕获 + 续作回 UI 线程 |
| 业务层 | TaskScheduler.Default (ThreadPool) | CPU 工作并发执行 |
| I/O 层 | N/A(HttpClient.SendAsync 走 IOCP) | 真异步,不占 worker thread |
| 进度反馈 | Progress<T> 内部捕获上下文 | 自动回 UI 线程 + 节流 |
| 失败处理 | 自定义后台线程 / Channel 消费 | 不阻塞业务路径 |
🛡️ 4 个陷阱的应对
1. **线程池饥饿** → 用真异步 I/O(HttpClient.SendAsync)+ 不滥用 Task.Run
2. **UI 线程死锁** → 业务代码全部 .ConfigureAwait(false) + UI 层用 await 而非 .Wait()
3. **异常吞没** → 每个 Task 独立 try/catch + failureWriter 显式上报 + Task.WhenAll 聚合
4. **进度反馈抖动** → Progress<T> 内部用 SynchronizationContext 节流(不会每 1ms 都更新 UI)+ Channel 背压
📊 性能数据(参考)
| 方案 | 5 个服务(总耗时) | UI 卡顿 | 线程池占用 |
| ----------------------------- | -------------------- | ------------ | --------------- |
| 同步顺序调用 | 5 × 200 ms = 1000 ms | 1000 ms 完全卡死 | 1 worker |
| Task.Run × 5 + .Wait() | ~200-300 ms | 死锁 ❌ | 5 worker + UI 卡 |
| Task.WhenAll + ConfigureAwait | ~200-300 ms | 60 fps 流畅 | 0 worker(IOCP) |
| SemaphoreSlim(3) 限流 | ~300-400 ms | 60 fps 流畅 | 0 worker(IOCP) |
**关键洞察**:HttpClient.SendAsync 走 IOCP(I/O 完成端口),**不占 worker thread**,所以即使 50 个并发请求也是零 worker 占用。线程池饥饿的真正杀手是 Task.Run + 同步 I/O 滥用。
───
**第 5 段结束。下条发易错点提醒 + 扩展思考 + 收尾。**
---
<!-- message_id: om_x100b68a51b694934b297a332b6eba56 -->
四、易错点提醒(10 条)
1. **await Task.Run(...).Result 在 UI 线程死锁** —— WPF 项目最常见坑,用 await 替代 .Result
2. **Task.Run(() => File.ReadAllText(...)) 假异步** —— 用 File.ReadAllTextAsync 真异步
3. **TaskCreationOptions.LongRunning 不是优化开关** —— 它是给 CLR 的"长任务"暗示,滥用 = 退化到 new Thread
4. **ThreadPool.SetMinThreads(100, 100) 解决并发 I/O 是错的** —— I/O 走 IOCP,不占 worker
5. **Task.Delay(1000).Wait() 占线程池** —— 用 await Task.Delay(1000) 或 Thread.Sleep
6. **Task.Run 内调用同步方法占池线程** —— 长同步工作直接 new Thread 或 LongRunning
7. **await 后未 ConfigureAwait(false)** —— 库代码应一律 ConfigureAwait(false),避免上下文捕获开销
8. **Task.Run 内抛异常没被观察** —— Task.IsFaulted + await 才能正确传播
9. **Parallel.ForEachAsync 默认并发度 = 处理器数** —— CPU-bound 友好,I/O-bound 浪费,需要 MaxDegreeOfParallelism
10. **CancellationTokenSource 没 Dispose** —— 实现 IDisposable,用 using 包装
五、扩展思考(2 个引导题)
1. **动手验证**:写一段代码测试 ThreadPool.SetMinThreads(8, 8) 对 100 个并发 HttpClient.SendAsync 请求的影响,对比 SetMinThreads(1, 1) —— 你会观察到两者延迟几乎一致(因为 I/O 不占 worker)。这说明什么?
2. **架构思考**:如果把 5 个微服务调用改成 500 个(突发流量),架构要如何调整?
- 提示:从 Channel 背压 + SemaphoreSlim 限流 + Polly 重试 + 熔断器 四个角度思考
六、关联今日内容 → 网络图
Day 4 (async/await 状态机)
↓ 续作由 TaskScheduler 调度
Day 8 (GC)
↓ worker thread 是 GC 根
Day 9 (JIT)
↓ 线程越多 JIT 缓存压力越大
Day 10 (异常)
↓ worker 异常 .NET Core 2+ 终止进程
Day 11 (今日 · 线程池)
↓ 并发原语的基础设施
Day 12 (下次 · 锁机制)
↓ Monitor / SemaphoreSlim / AsyncLock
Day 13 (并行编程)
↓ Parallel / PLINQ / Channels
✅ **Day 11 推送完成**
今日要点速记:
- 🎯 线程池 = 线程复用 + 自适应并发 + 工作窃取 + Starvation Avoidance
- 🎯 TaskScheduler = 调度策略解耦(线程池 / UI 线程 / 自定义)
- 🎯 LongRunning = 独立线程不入池(不是优化开关)
- 🎯 WPF 中 await 自动捕获 DispatcherSynchronizationContext → 续作回 UI
- 🎯 真异步 I/O(HttpClient)走 IOCP → 不占 worker thread
下次 Day 12:**锁机制(Monitor / SemaphoreSlim / AsyncLock)** —— 线程池之上的并发原语,包括锁的内存模型语义、async 友好的锁、CancellationToken 集成、ReaderWriterLockSlim 的使用陷阱。
📝 **请主人回复**:
1. Q1/Q2/Q3 你觉得难度如何?
2. 「工作窃取 LIFO + 偷取 FIFO」这个细节以前知道么?
3. Q3 的 5 层架构里,有哪个层你想深入拆解的?
4. 扩展思考第 2 题(500 个并发如何调整)有想法么?
我会根据你的反馈**明天降维拆解或直接推进**。