结构化并发的本质是“谁启动,谁负责等完”,即子任务必须在父作用域内启动、完成或取消,异常必须向上传播;java 24 的 structuredtaskscope 通过 try-with-resources 和 throwiffailed() 实现异常自动上抛与任务协同终止。

结构化并发的本质是“谁启动,谁负责等完”
结构化并发不是新语法糖,而是对并发任务生命周期的硬性约束:子任务必须在父作用域内启动、完成或被取消,异常必须向上传播,不能静默吞掉。这直接回答了你关心的问题——父任务不是靠“监听”或“轮询”来管理子任务异常,而是通过作用域机制自动捕获、聚合并决定是否终止整个并发块。
StructuredTaskScope(Java 24)如何让异常自动上抛
Java 24 的 StructuredTaskScope 是最直白的落地实现:它把多个 fork() 出的任务绑在一个 try-with-resources 块里,join() 不会阻塞到所有任务自然结束,而是一旦任一任务抛出异常,就立即中断其余任务;调用 throwIfFailed() 会把第一个失败任务的原始异常封装进 ExecutionException 抛出。
-
scope.fork()启动的任务共享同一取消上下文,父线程中断即全部中断 - 子任务中 throw 的
IOException或RuntimeException不会被吞,throwIfFailed()可直接拿到e.getCause() - 不调用
throwIfFailed()就等于没检查异常——这是新手最常漏的一步 - 如果子任务用
Thread.sleep()阻塞但没响应中断,会拖慢整个作用域退出,必须显式检查Thread.interrupted()
Go 的 errgroup.Group 为什么比裸 go 更可靠
裸写 go func() { ... }() 时,子 goroutine panic 或 return error 完全不会影响主流程;而 errgroup.Group 用一个共享 context.Context 统一控制生命周期,并在 g.Wait() 时返回第一个非 nil 错误,同时触发上下文取消,让其他子任务能感知并退出。
- 子任务中必须主动 select
,否则无法响应取消,变成“僵尸协程” -
g.Go()内部函数签名强制为func() error,从类型上杜绝“只 log 不返回错误”的惯性操作 - 如果某个子任务 panic 而非 return error,
errgroup捕获不到——得靠recover()包一层再转成 error - 多个子任务都失败时,
g.Wait()只返回第一个错误,后续错误被丢弃;如需聚合,得自己维护 error 切片
Kotlin 协程里 coroutineScope 和 supervisorScope 的关键区别
两者都创建作用域,但异常传播策略完全不同:coroutineScope 是严格结构化——任一子协程异常,整个作用域立即取消,其他子协程被中断;supervisorScope 则允许子协程独立失败,不干扰兄弟任务,适合“尽力而为”场景(比如上报日志+继续处理主逻辑)。
- 用
launch启动的子协程,在coroutineScope中抛出未捕获异常,会直接导致coroutineScope块抛出CancellationException -
supervisorScope下的子协程异常不会传播,但也不会自动被清理——你得自己 handle 或加catch块 - 别在
supervisorScope里混用async然后不 await,结果可能拿不到异常,因为async的异常只在await()时才抛 - 作用域嵌套时,外层是
coroutineScope,内层是supervisorScope,异常仍会被外层拦截——传播路径由最近的非 supervisor 作用域决定
真正难的不是写对某一行代码,而是理解“作用域”不是语法糖,而是一种契约:你声明了父子关系,就得接受它带来的约束——取消要联动,异常要冒泡,资源要守恒。一旦绕过作用域(比如在 scope 外 launch、用全局线程池、手动 new Thread),结构化就崩了。











