第6章:异步、多线程、任务和并行(建议71-89)
覆盖异步与线程场景、Semaphore/lock、线程生命周期/优先级/停止和数量、ThreadPool/Task,以及Parallel、PLINQ、异常、桌面UI线程、加速边界与锁竞争。
学习目标
- 能区分I/O asynchrony、CPU parallelism、concurrency和thread affinity,设计Task、Thread、Parallel或UI dispatcher执行模型
- 能比较SemaphoreSlim、private lock、background/priority flags与CancellationToken,判断同步identity、并发预算和停止协议
- 能分析Parallel、PLINQ与Task.WhenAll的partition、ordering、exception和shared-state成本,并计算是否值得并行
机制总览
第6章:异步、多线程、任务和并行(建议71-89):机制路径
- 1
为什么“更并发”不等于“更快”或“更异步”
async让等待不占用调用thread,multithreading允许多个execution flows,parallelism让多个CPU cores同时计算;三者可以组合,也可以完全独立。一个async file call可能只有一个thread在执行,一个CPU loop可以用Paralle…
- 2
Execution Model与Thread Lifecy…
I/O-bound operation使用真正的async API,让OS completion在结果就绪时恢复continuation;不要用 Task.Run 包同步database/network调用假装scalable。CPU-bound work若需要保持UI/request thread…
- 3
Synchronization Identity与Coor…
Semaphore/SemaphoreSlim最适合限制同时进入某resource的数量,例如最多8个remote calls;初始count为1时也可互斥,但不自动表达复杂object invariant。process内async flow优先 SemaphoreSlim.WaitAsync ,…
章级决策实验
第6章:异步、多线程、任务和并行(建议71-89):机制与证据
切换《第6章:异步、多线程、任务和并行(建议71-89)》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么“更并发”不等于“更快”或“更异步”
async让等待不占用调用thread,multithreading允许多个execution flows,parallelism让多个CPU cores同时计算;三者可以组合,也可以完全独立。一个async file call可能只有一个thread在执行,一个CPU loop可以用Paralle…
可核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「为什么“更并发”不等于“更快”或“更异步”」的收益与反例。
学完《第6章:异步、多线程、任务和并行(建议71-89)》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
第6章:异步、多线程、任务和并行(建议71-89):失效与核验
为什么“更并发”不等于“更快”或“更异步”
典型失效
若把「为什么“更并发”不等于“更快”或“更异步”」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。
核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「为什么“更并发”不等于“更快”或“更异步”」的收益与反例。
Execution Model与Thread Lifecy…
典型失效
若把「Execution Model与Thread Lifecy…」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。
核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「Execution Model与Thread Lifecy…」的收益与反例。
Synchronization Identity与Coor…
典型失效
若把「Synchronization Identity与Coor…」当作脱离版本与上下文的硬规则,可能用过时的优化或风格替换了更重要的正确性、安全性与可维护性约束。
核验证据
固定当前 .NET、语言版本和输入规模,用编译诊断、分析器、自动化测试、基准或安全失败样本复核「Synchronization Identity与Coor…」的收益与反例。
为什么“更并发”不等于“更快”或“更异步”
async让等待不占用调用thread,multithreading允许多个execution flows,parallelism让多个CPU cores同时计算;三者可以组合,也可以完全独立。一个async file call可能只有一个thread在执行,一个CPU loop可以用Parallel但没有async,UI app还必须把最终mutation送回single UI thread。
先预测:启动Thread后下一行能否假设它已执行;把Thread设为background能否保证程序退出前finally运行;Parallel loop里每次都锁同一个gate是否仍会加速。三个答案都是否。设计从workload、ownership和budget开始,不从API名字开始。
↡系统允许同时in-flight的operations、threads、tasks、connections和CPU workers上限,以及达到上限后的等待/拒绝策略。Execution Model与Thread Lifecycle(建议71、74-80)
建议71:区分异步和多线程应用场景
I/O-bound operation使用真正的async API,让OS completion在结果就绪时恢复continuation;不要用Task.Run包同步database/network调用假装scalable。CPU-bound work若需要保持UI/request thread responsive,可在明确application boundary调度到ThreadPool;若要加速,需数据可分、work足够大且core有余量。
public async Task<OrderDto> LoadAsync(OrderId id, CancellationToken cancellationToken)
{
Order order = await _repository.LoadAsync(id, cancellationToken).ConfigureAwait(false);
return Map(order);
}异步不承诺换thread,也不承诺并行;await前后在哪个context恢复取决于environment与awaitable。用latency、throughput、worker count和downstream saturation验收。
建议74:警惕线程的IsBackground
foreground thread会阻止process正常退出,background thread不会;process只剩background threads时runtime可以结束它们,不保证finally、flush或transaction完成。因此IsBackground不是lifecycle management,critical work必须由host拥有、发出cancellation并await/join到明确deadline。
using var shutdown = new CancellationTokenSource();
Task worker = RunWorkerAsync(shutdown.Token);
shutdown.Cancel();
await worker.WaitAsync(TimeSpan.FromSeconds(10));建议75:警惕线程不会立即启动
Thread.Start只让thread进入可调度状态,实际运行时刻由OS scheduler决定;创建Task也不代表continuation立刻执行。不能用Sleep“等它启动”,应使用Task completion、event/signal或barrier表达ready handoff。
var ready = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
Thread thread = new(() =>
{
InitializeThreadOwnedState();
ready.SetResult();
RunLoop();
});
thread.Start();
await ready.Task;建议76:警惕线程的优先级
ThreadPriority是scheduler hint,不是correctness、deadline或ordering guarantee,且平台行为不同。提高priority可能starve其他work,降低priority也可能让shutdown迟迟不完成。real-time需求要用相应OS/runtime机制与完整资源预算,普通app先修复blocking、contention和work granularity。
建议77:正确停止线程
使用cooperative cancellation:owner请求停止,worker在safe points观察token,退出loop、释放资源并由owner await/join。不要使用Thread.Abort之类的异步强杀来打断任意instruction,它会破坏invariant;blocking API也必须支持token、timeout或可关闭handle。
↡owner发出CancellationToken信号,worker在可保持invariant的safe point观察、unwind并由owner等待完成的停止协议。while (!cancellationToken.IsCancellationRequested)
{
WorkItem item = await channel.Reader.ReadAsync(cancellationToken);
await ProcessAsync(item, cancellationToken);
}把cancellation与failure分开:host-requested OperationCanceledException不是error;partial side effects、stop latency和cleanup仍需测试。
建议78:应避免线程数量过多
每个OS thread消耗stack、scheduler bookkeeping与context switching;CPU-bound runnable threads远多于cores通常降低throughput,blocking threads过多则耗尽ThreadPool并推高latency。使用bounded Channel/Semaphore、connection pool和rate limit建立end-to-end concurrency budget,不要为每个item创建Thread。
建议79:使用ThreadPool或BackgroundWorker代替Thread
对短期、无thread affinity的work复用ThreadPool,避免手工thread成本;BackgroundWorker是旧WinForms/WPF event-based compatibility API,新代码更适合Task、CancellationToken和IProgress<T>。只有COM apartment、长期blocking pump、native callback affinity等明确需求才创建dedicated Thread。
建议80:用Task代替ThreadPool
Task是可组合的async result/fault/cancellation abstraction,支持await、WhenAll、continuations和structured ownership,比直接QueueUserWorkItem更容易观察完成与异常。Task不等于Thread:async Task可在等待期间不占worker,同一thread也会执行多个tasks。
Task<Report> reportTask = BuildReportAsync(input, cancellationToken);
Task<Summary> summaryTask = BuildSummaryAsync(input, cancellationToken);
await Task.WhenAll(reportTask, summaryTask);
return new Package(await reportTask, await summaryTask);避免fire-and-forget;每个Task要被caller await或交给明确supervisor。
切换I/O、CPU、dedicated thread、UI与legacy worker
Synchronization Identity与Coordination(建议72-73)
建议72:在线程同步中使用信号量
Semaphore/SemaphoreSlim最适合限制同时进入某resource的数量,例如最多8个remote calls;初始count为1时也可互斥,但不自动表达复杂object invariant。process内async flow优先SemaphoreSlim.WaitAsync,cross-process命名semaphore才用OS primitive。
private readonly SemaphoreSlim _slots = new(initialCount: 8, maxCount: 8);
public async Task<Result> CallAsync(Request request, CancellationToken cancellationToken)
{
await _slots.WaitAsync(cancellationToken);
try { return await _client.SendAsync(request, cancellationToken); }
finally { _slots.Release(); }
}只有成功wait后才release,防止count漂移;dispose lifecycle也要与所有waiters协调。
建议73:避免锁定不恰当的同步对象
lock identity必须private、stable且只由同一invariant owner持有。不要lock this、public object、typeof(T)、interned string或可替换field,外部代码可能拿同一identity造成deadlock/priority inversion。lock scope内不做I/O、长计算或await。
private readonly object _gate = new();
public void Add(Balance delta)
{
lock (_gate)
_balance = _balance.Add(delta);
}lock保护invariant而非“某个变量”。先列出被共同更新的fields和合法state,再选single owner、immutable snapshot、Interlocked或lock。
切换Semaphore、lock、background、priority与cancellation
Parallel、PLINQ、Fault与UI Boundary(建议81-89)
建议81:使用Parallel简化同步状态下Task的使用
Parallel.For/ForEach适合finite、synchronous、CPU-bound且iterations相对独立的loop,runtime负责partition和worker scheduling,比手工创建一组Tasks更简洁。它通常同步阻塞caller直到完成,不适合body里await I/O;async fan-out用Task/WhenAll并设置并发上限。
Parallel.ForEach(
partitions,
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
partition => Compute(partition));建议82:Parallel简化但不等同于Task默认行为
Parallel API可能使用calling thread参与工作,采用dynamic partitioning并在调用返回前汇合;Task代表单个可await completion,不天然要求parallel execution。scheduler、blocking、cancellation和exception shape都不同,不能把Parallel.For理解成Task.WhenAll语法糖。
建议83:小心Parallel中的陷阱
iteration order默认不保证;shared counter、Random实例、non-thread-safe collection和external side effects会race;tiny body的partition/scheduling overhead可能超过work。使用thread-local state与final reducer,或让每个partition产生独立result再合并。
long total = 0;
Parallel.ForEach(
ranges,
() => 0L,
(range, _, local) => local + Sum(range),
local => Interlocked.Add(ref total, local));建议84:使用PLINQ
PLINQ适合large、pure、in-memory query,AsParallel后provider负责partition/merge。ordering默认可放弃以提速,需要稳定顺序时AsOrdered会增加成本。不要对IQueryable remote provider混淆PLINQ,也不要在query中做I/O或mutable side effects。
Result[] results = values
.AsParallel()
.WithDegreeOfParallelism(Environment.ProcessorCount)
.Select(ComputePureResult)
.ToArray();建议85:Task中的异常处理
Task fault被保存在Task中,await会在await point重新抛出;必须await/observe每个owned Task。WhenAll用于等待全部完成,但需要完整fault inventory时还应检查combined task/individual tasks,不要只处理第一条表象。OperationCanceledException按matching token与policy分类。
Task all = Task.WhenAll(tasks);
try
{
await all;
}
catch
{
IReadOnlyList<Exception> faults = tasks
.Where(task => task.IsFaulted)
.SelectMany(task => task.Exception!.Flatten().InnerExceptions)
.ToArray();
throw new BatchFailedException(faults);
}建议86:Parallel中的异常处理
多个iterations可能同时失败,Parallel/PLINQ通常以AggregateException呈现;catch应在parallel operation boundary,flatten/classify已启动工作的faults,并决定是否整个operation失败。不要在每个iteration吞异常继续污染shared state;需要per-item result时让body返回显式outcome。
建议87:区分WPF和WinForm的线程模型
两者都要求UI object主要由创建它的UI thread访问,但marshalling API不同:WPF使用Dispatcher/DispatcherObject,WinForms使用Control.Invoke/BeginInvoke及其modern async variants;SynchronizationContext可提供共同抽象。后台work只计算data,最终最小UI mutation回到UI context。
DataModel model = await LoadAndComputeAsync(cancellationToken);
await dispatcher.InvokeAsync(() => ViewModel.Apply(model));不要在UI thread .Result/.Wait()阻塞等待可能capture context的Task,会freeze甚至deadlock。library code不应依赖UI context,application layer负责marshal。
建议88:并行并不总是速度更快
speedup受sequential fraction、partition、merge、cache/memory bandwidth、allocation与contention限制;small input和cheap body常被overhead吞没。必须与optimized sequential baseline比较,并在representative hardware/data上测wall time、CPU、GC和tail latency。
↡每个parallel work item包含的有效计算量相对调度、partition、synchronization和merge overhead的比例。建议89:在并行方法体中谨慎使用锁
所有iterations竞争同一lock会把parallel loop串行化并增加context switching。优先thread-local accumulator、partitioned data、immutable input、Interlocked或concurrent collection;若必须lock,缩小protected state与hold time,并用contention trace证明收益仍为正。
var locals = new ConcurrentBag<PartialResult>();
Parallel.ForEach(partitions, partition =>
{
PartialResult local = Compute(partition);
locals.Add(local);
});
Result result = Merge(locals);切换Parallel、WhenAll、PLINQ、UI marshal与parallel lock
本章回顾:先定Work Shape,再定Execution Primitive
- async处理wait,Parallel处理可分CPU work,Thread只为明确affinity/lifecycle需求;Task是completion/fault abstraction而非thread别名。
- IsBackground、Start和Priority都不是correctness guarantee;owner通过token请求停止并await/join完成。
- Semaphore限制并发数,private lock保护短invariant;两者都服从end-to-end concurrency budget。
- Parallel同步汇合且partition loop,Task.WhenAll组合async operations,PLINQ处理large pure in-memory query。
- Task/Parallel faults必须完整observe,UI mutation回到对应dispatcher,未知fault由owner boundary决策。
- parallel speedup必须超过partition/merge/contention成本,shared lock往往是最直接的反证。
练习
问题 1:一个Web API要并发调用1000个远程服务请求,应使用Parallel.For还是Task.WhenAll,怎样限流?
问题 2:dedicated worker thread如何证明可以正确启动和停止?
问题 3:怎样判断一个Parallel或PLINQ改写真的更快且仍正确?
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- concurrency budget
- cooperative cancellation
- synchronization identity
- execution context
- parallel grain
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
建议72:在线程同步中使用信号量:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议72:在线程同步中使用信号量必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。
建议73:避免锁定不恰当的同步对象:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议73:避免锁定不恰当的同步对象必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。
建议74:警惕线程的IsBackground:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议74:警惕线程的IsBackground必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。
建议75:警惕线程不会立即启动:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议75:警惕线程不会立即启动必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。
建议76:警惕线程的优先级:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议76:警惕线程的优先级必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。
建议77:正确停止线程:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议77:正确停止线程必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。
建议78:应避免线程数量过多:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议78:应避免线程数量过多必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。
建议79:使用ThreadPool或BackgroundWorker代替Thread:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议79:使用ThreadPool或BackgroundWorker代替Thread是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议80:用Task代替ThreadPool:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议80:用Task代替ThreadPool必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。
建议81:使用Parallel简化同步状态下Task的使用:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议81:使用Parallel简化同步状态下Task的使用必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。
建议82:Parallel简化但不等同于Task默认行为:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议82:Parallel简化但不等同于Task默认行为必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。
建议83:小心Parallel中的陷阱:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议83:小心Parallel中的陷阱是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。
建议84:使用PLINQ:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议84:使用PLINQ涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。
建议85:Task中的异常处理:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议85:Task中的异常处理必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。
建议86:Parallel中的异常处理:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议86:Parallel中的异常处理跨越输入信任或资源生命周期边界,建议只有在明确威胁模型、所有权和失败路径后才成立。用恶意/畸形输入、失败注入和资源计数检查拒绝行为、敏感数据暴露与最终释放,而不是只验证顺利路径。
建议87:区分WPF和WinForm的线程模型:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议87:区分WPF和WinForm的线程模型必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。
建议88:并行并不总是速度更快:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议88:并行并不总是速度更快必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。
建议89:在并行方法体中谨慎使用锁:机制、边界与证据
- 异步、多线程、任务和并行(建议71-89)中的建议89:在并行方法体中谨慎使用锁必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。