第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. 1

    为什么“更并发”不等于“更快”或“更异步”

    async让等待不占用调用thread,multithreading允许多个execution flows,parallelism让多个CPU cores同时计算;三者可以组合,也可以完全独立。一个async file call可能只有一个thread在执行,一个CPU loop可以用Paralle…

  2. 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. 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名字开始。

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。

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。

分步1 / 3

切换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。

分步1 / 3

切换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。

建议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);
分步1 / 3

切换Parallel、WhenAll、PLINQ、UI marshal与parallel lock

本章回顾:先定Work Shape,再定Execution Primitive

  1. async处理wait,Parallel处理可分CPU work,Thread只为明确affinity/lifecycle需求;Task是completion/fault abstraction而非thread别名。
  2. IsBackground、Start和Priority都不是correctness guarantee;owner通过token请求停止并await/join完成。
  3. Semaphore限制并发数,private lock保护短invariant;两者都服从end-to-end concurrency budget。
  4. Parallel同步汇合且partition loop,Task.WhenAll组合async operations,PLINQ处理large pure in-memory query。
  5. Task/Parallel faults必须完整observe,UI mutation回到对应dispatcher,未知fault由owner boundary决策。
  6. 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:在线程同步中使用信号量:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议72:在线程同步中使用信号量必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。

建议73:避免锁定不恰当的同步对象:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议73:避免锁定不恰当的同步对象必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。

建议74:警惕线程的IsBackground:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议74:警惕线程的IsBackground必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。

建议75:警惕线程不会立即启动:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议75:警惕线程不会立即启动必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。

建议76:警惕线程的优先级:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议76:警惕线程的优先级必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。

建议77:正确停止线程:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议77:正确停止线程必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。

建议78:应避免线程数量过多:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议78:应避免线程数量过多必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。

建议79:使用ThreadPool或BackgroundWorker代替Thread:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议79:使用ThreadPool或BackgroundWorker代替Thread是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议80:用Task代替ThreadPool:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议80:用Task代替ThreadPool必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。

建议81:使用Parallel简化同步状态下Task的使用:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议81:使用Parallel简化同步状态下Task的使用必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。

建议82:Parallel简化但不等同于Task默认行为:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议82:Parallel简化但不等同于Task默认行为必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。

建议83:小心Parallel中的陷阱:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议83:小心Parallel中的陷阱是一条需要上下文的工程建议,而不是无条件规则。先声明适用的语言/运行时版本、代码约束与反例,再以编译器诊断、分析器、自动化测试或基准结果证明采用该建议确实改善正确性、可维护性或性能。

建议84:使用PLINQ:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议84:使用PLINQ涉及类型语义、分配和枚举成本,不能凭代码长度或旧版微优化判断。固定运行时、构建模式和输入规模,用基准、分配统计与多组边界数据比较替代方案,同时验证结果语义一致。

建议85:Task中的异常处理:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议85:Task中的异常处理必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。

建议86:Parallel中的异常处理:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议86:Parallel中的异常处理跨越输入信任或资源生命周期边界,建议只有在明确威胁模型、所有权和失败路径后才成立。用恶意/畸形输入、失败注入和资源计数检查拒绝行为、敏感数据暴露与最终释放,而不是只验证顺利路径。

建议87:区分WPF和WinForm的线程模型:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议87:区分WPF和WinForm的线程模型必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。

建议88:并行并不总是速度更快:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议88:并行并不总是速度更快必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。

建议89:在并行方法体中谨慎使用锁:机制、边界与证据

  1. 异步、多线程、任务和并行(建议71-89)中的建议89:在并行方法体中谨慎使用锁必须区分任务生命周期、线程调度、共享状态与异常传播,旧版本经验不能直接当作当前运行时保证。用受控取消、超时和竞争样本记录任务状态、异常与资源释放,并以压力测试确认结论在重复调度下仍成立。

讨论

评论区加载中…