Chapter 13. Working with asynchronous computations
覆盖Task composition、Traverse/Sequence与Task+Either effects,显式处理启动、并发上限、ordering、cancellation和fault taxonomy。
学习目标
- 能区分async function、cold invocation与hot Task,分析start、await、fault和cancellation的observable lifecycle
- 能设计Sequence/Traverse的sequential、parallel与bounded执行策略,写出ordering和failure contract
- 能实现Task与Either的组合helper,判断transport fault、cancellation、validation和business rejection属于哪一层
机制总览
Chapter 13. Working with asynchronous computations:机制路径
- 1
为什么Async的难点不是少写Callback
async/await 改善了控制流语法,但没有替你决定工作何时启动、可否重复、并发多少、错误如何分类、取消是否传播。
- 2
Asynchronous computations
Async method通常在调用时同步执行到第一个incomplete await,并返回一个hot Task;再次调用method会创建另一份工作,而再次await同一个Task只观察同一份工作。API要区分 Func<CancellationToken, Task<T>>…
- 3
Failure, cancellation, and ti…
Timeout可能来自client deadline、HTTP stack、database command或自定义race;它不总等于cancellation,也不必然retryable。建立matrix记录source、exception/outcome、是否side effect commit…
章级决策实验
Chapter 13. Working with asynchronous computations:机制与证据
切换《Chapter 13. Working with asynchronous computations》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么Async的难点不是少写Callback
async/await 改善了控制流语法,但没有替你决定工作何时启动、可否重复、并发多少、错误如何分类、取消是否传播。
可核验证据
以确定输入重复运行「为什么Async的难点不是少写Callback」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
学完《Chapter 13. Working with asynchronous computations》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
Chapter 13. Working with asynchronous computations:失效与核验
为什么Async的难点不是少写Callback
典型失效
若把「为什么Async的难点不是少写Callback」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「为什么Async的难点不是少写Callback」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
Asynchronous computations
典型失效
若把「Asynchronous computations」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「Asynchronous computations」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
Failure, cancellation, and ti…
典型失效
若把「Failure, cancellation, and ti…」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「Failure, cancellation, and ti…」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
为什么Async的难点不是少写Callback
async/await改善了控制流语法,但没有替你决定工作何时启动、可否重复、并发多少、错误如何分类、取消是否传播。Task<T>既是未来结果也是正在运行/已完成工作的handle;调用返回Task的function与持有一个Task语义不同。函数式组合的目标是让这些effect仍能Map/Bind,同时不掩盖lifecycle policy。
先预测:创建Task变量一定启动工作吗;两个await同一Task会执行两次吗;Select(async ...)会自动限制并发吗;Task.WhenAll遇一项失败会取消其他项吗;把validation Left放进faulted Task是否更统一。答案都是否。
Asynchronous computations
Async method通常在调用时同步执行到第一个incomplete await,并返回一个hot Task;再次调用method会创建另一份工作,而再次await同一个Task只观察同一份工作。API要区分Func<CancellationToken, Task<T>> recipe与Task<T> handle。前者可控制何时开始并传token,后者可能在composition前已经运行。
async Task<Either<Error, Customer>> LoadCustomer(
CustomerId id,
CancellationToken token)
{
var row = await gateway.Find(id, token).ConfigureAwait(false);
return row is null
? Left(Error.CustomerNotFound)
: Right(Map(row));
}Async composition的Bind需要await outer value,再调用next function;若outer canceled/faulted,不应执行next。Library code通常避免依赖captured synchronization context,application UI boundary则可能需要回到dispatcher。不要机械添加Task.Run包装I/O,它占用thread-pool却不改变remote latency;CPU-bound work是否offload由caller context和capacity policy决定。
切换cold function、hot task、cancel与fault
Failure, cancellation, and timeout taxonomy
Timeout可能来自client deadline、HTTP stack、database command或自定义race;它不总等于cancellation,也不必然retryable。建立matrix记录source、exception/outcome、是否side effect committed、retry budget和external code。Cancellation表示caller不再需要结果,通常向上传播;domain validation是成功完成的Task中的Left;unexpected infrastructure fault保留fault channel和cause。
Retry包围的是重新调用function,不是重新await同一faulted Task。每次attempt创建新resource/token child,并以idempotency key防止unknown completion重复effect。Backoff需要jitter、deadline和max attempts;若outer request已取消,retry loop立刻停止。Telemetry按operation id串联attempts,同时保留每次duration/outcome。
Traversables: working with lists of elevated values
Map async function overlist得到IEnumerable<Task<A>>,但多数caller想要Task<IReadOnlyList<A>>。Sequence把结构翻转;Traverse等价于Map后Sequence。Type transformation不决定execution policy:eager LINQ projection可能先启动全部tasks,sequential loop一次一个,bounded worker限制in-flight count。
async Task<IReadOnlyList<B>> TraverseBounded<A, B>(
IEnumerable<A> source,
int capacity,
Func<A, CancellationToken, Task<B>> map,
CancellationToken token)
{
using var gate = new SemaphoreSlim(capacity);
var tasks = source.Select(async (item, index) =>
{
await gate.WaitAsync(token);
try { return (index, Value: await map(item, token)); }
finally { gate.Release(); }
});
return (await Task.WhenAll(tasks))
.OrderBy(x => x.index).Select(x => x.Value).ToArray();
}Policy需说明output是否保持input order、first fault后是否停止启动新项、已启动项是否cancel、多个fault如何呈现、empty list返回什么。Task.WhenAll不会自动撤销已开始的external effects;fail-fast只是caller较早得到failure。对rate-limited service,应使用bounded scheduler/Channel并观察queue length,而非一次创建十万tasks再用WhenAll。
切换sequential、parallel、bounded与failure
Independent validation versus dependent async work
若每项validation彼此独立,可并行执行并accumulate all typed errors;若下一步需要前一步产生的id,则必须Bind顺序执行。把dependent chain硬改为WhenAll会缺少input,把independent checks写成Bind则只返回第一个错误且增加latency。Applicative与monadic composition表达的是dependency graph,execution scheduler再决定并发。
对批量command还需定义partial success:all-or-nothing transaction、每项result、或accepted job。不要用一个exception代表“其中一项业务失败”;返回indexed outcomes并保留input identity。若要求atomic remote effects,单机Task composition无法提供distributed transaction,应采用reservation/saga/idempotent compensation等protocol。
Combining asynchrony and validation (or any other two monadic effects)
Task<Either<Error,A>>有两层effect:Task表达尚未完成/fault/cancel,Either表达完成后的expected domain outcome。普通Task Bind只解一层;若每个service function都返回stacked type,需要TaskEither helper:await outer,Left直接返回,Right才调用next。Map只变A,MapLeft只变domain error,RecoverFault只处理约定infrastructure fault。
async Task<Either<E, B>> BindAsync<E, A, B>(
Task<Either<E, A>> source,
Func<A, Task<Either<E, B>>> next)
{
var outcome = await source.ConfigureAwait(false);
return outcome.Match(
Left: error => Task.FromResult(Left<E, B>(error)),
Right: next);
}Stack order取决于semantics。Either<E,Task<A>>可在async work创建前立即拒绝,但一旦Right,later Task仍可fault;Task<Either<E,A>>更常用于远程workflow。Validation accumulation与Task cancellation组合时,要明确一项fault是否阻止其他validation,是否保留已完成errors。不要创建“万能Effect”把所有channels压成字符串。
切换valid、invalid、fault与compose
Concurrency budgets and structured ownership
每个并发operation都应属于一个scope:request、batch或background worker。Scope拥有deadline、token、capacity和terminal join,退出前处理所有children,避免fire-and-forget泄露。若确需后台任务,交给durable queue/hosted service并持久化ownership,不把未观察Task丢在request thread上。
Capacity从下游限制推导:DB pool、HTTP rate limit、CPU cores、memory per item。Metrics记录queue wait、in-flight、duration、fault/cancel/retry和downstream saturation。Load test逐步增加source size,观察bounded traverse峰值稳定;unit test通过fake scheduler验证语义,二者不可互相替代。
Production gate: prove every terminal path
发布前覆盖success、domain Left、timeout before send、cancel during I/O、fault after remote commit、retry exhausted和process shutdown。每个path断言resource disposed、child tasks observed、token propagated、idempotency key stable、external response与internal cause分离。对batch还要验证empty/one/many、input order与duplicate identity。
API review逐个标注function还是hot Task,是否允许multiple invocation,是否线程安全,哪个layer捕获exception。只要这些问题仍靠“大家知道”,composition就没有形成contract。
本章回顾:类型保留Effect,Policy决定执行
- Async function invocation与Task handle不同;重复调用和重复await的effect count不同。
- Cancellation、domain rejection、timeout与unexpected fault需要独立语义。
- Traverse翻转list/effect结构,但sequential、parallel和bounded policy必须另行声明。
- Task-Either stack保留transport lifecycle与domain outcome两层,helper只能减少样板不能抹平层次。
- Concurrency必须有scope、capacity、deadline、idempotency和terminal-path evidence。
练习
问题 1:怎样证明Bounded Traverse最多只运行N项?
问题 2:Validation Left为何不应放进Faulted Task?
问题 3:Fault后Retry为什么必须重新调用Function?
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- task lifecycle contract
- cancellation propagation
- Traverse operation
- Task-Either stack
- concurrency budget