跳到正文
cccskills
免费在 GitHub 公开中文介绍

platform-coroutines-structured-concurrency

检查IntelliJ产品中的Kotlin协程代码,梳理作用域归属、取消与失败的传播方式,并确认长任务和Flow收集能否响应取消,针对泄漏或取消遗漏提出结构调整建议。

原文查看原文说明

Write or review IntelliJ coroutine scope ownership and cancellation.

JetBrains/intellij-community星标 20,685(GitHub 20,685 + cccskills 0)

实测评分 未实测

查看安装方法

适用场景

  • 审查协程作用域归属
  • 排查任务泄漏或取消遗漏
  • 检查Flow收集的停止处理

中文介绍

功能

此技能用于检查IntelliJ产品中的Kotlin协程代码,梳理每个作用域由哪个对象拥有,以及何时会被取消。它会检查取消和失败如何在父子协程间传播,并确认长时间运行的任务或Flow收集是否配合取消。发现问题时,会说明影响并提出较小的结构调整建议。检查对象包括GlobalScope、脱离父级的作用域、被吞掉的取消异常,以及试图用外层try捕获launch失败的代码。

适用场景

添加协程,或排查任务泄漏、无法停止的循环、取消后的状态不一致时,可以使用此技能。它也适用于希望让任务生命周期与服务或界面生命周期关联的开发者。

使用示例

  • “请确认这个协程作用域由谁拥有,以及何时取消。”
  • “请检查这段异常处理是否会吞掉取消信号。”
  • “请排查这个Flow收集过程是否遗漏了取消处理。”

注意事项

适用范围是IntelliJ产品中的Kotlin协程代码。界面或EDT(事件分发线程)的归属关系,以及协程内部机制的详细说明,由相关技能处理。

本介绍由 AI(OpenAI Codex)根据公开的 SKILL.md 撰写。准确的规格请查看下方原文。

文档检查

AI 根据 SKILL.md 的内容检查了需要注意的操作和使用前提。评分只依据实际运行的结果(实测对比)。

安全性发送数据、删除文件、执行命令等是否有说明
A良好内容以代码审查和修改建议为主,没有描述外部操作。
安装难易度所需工具和 API 密钥是否少且有说明
A良好除IntelliJ产品中的Kotlin代码外,未说明其他前提。

不计入评分。 由 AI(OpenAI Codex)于 2026年10月28日 评估。仅供参考,安装前请同时查看原文。

如果您认为评估有误,请申请复核

请写明哪一项有误以及原因。无需登录。我们会对照 SKILL.md 重新评估,并在此处更新结果。

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

包含的文件(1)

  • SKILL.md11.9 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

<!-- Generated by community/.ai/render-guides.mjs; edit community/.agents/skills/platform-coroutines-structured-concurrency/SKILL.md -->

Coroutines: structured concurrency

Keep cancellation, failures, and lifetimes predictable in Kotlin coroutine code across any IntelliJ-based product module. Three rules govern every decision:

  1. Hierarchy — which scope owns this coroutine, and when is that scope cancelled.
  2. Propagation — what a cancellation or failure does to parents, siblings, and children.
  3. Don't be creative — prefer standard primitives (coroutineScope, supervisorScope, a lifecycle-bound scope) over hand-rolled scope wiring, the most common source of leaks.

Reach for this skill when changing suspend functions, launch/async, CoroutineScope construction, cancellation handling, or Flow collection — and when diagnosing leaked coroutines, uncancellable loops, swallowed cancellation, GlobalScope, launch-in-init, detached scopes, invokeOnCompletion, ProcessCanceledException, blocking/progress bridges, ModalityState context capture, or promise/callback cancellation bridges.

Related skill: kotlin-ui-swing-component-architecture (Swing UI and EDT ownership).

Deeper reading on the platform internals:

  • The coroutines notebooks: Context Propagation, Cancellation, Read-Write Lock.
  • The Platform Docs of the ultimate checkout: docs/IntelliJ-Platform/4_man/Kotlin-Coroutines/ (services, EDT dispatchers, read actions), docs/IntelliJ-Platform/4_man/Performance/ (freezes, background write actions, profiling) and docs/IntelliJ-Platform/4_man/Indexes/ (the indexing system, dumb-aware code).

Core review loop

  1. Name the owning CoroutineScope and the exact moment it is cancelled (Disposable disposal, service teardown, explicit cancel()). If you cannot name that moment, that is the finding.
  2. Check long-running, CPU-bound, blocking, or looped coroutine bodies (and Flow collectors) for a cooperative cancellation checkpoint — do not flag short bodies that already suspend.
  3. Check every catch on the path for a swallowed CancellationException / ProcessCanceledException.
  4. Check any invokeOnCompletion, GlobalScope, manual CoroutineScope(...), or launch in init against the rules below, then classify (severity table) and propose the smallest structural fix.

Scope ownership and lifecycle

  • Never use GlobalScope for feature work — it is never cancelled, so its coroutines outlive the component that started them.

  • Prefer an injected or platform lifecycle scope (project/service scope, or the scope handed to you). A standalone CoroutineScope(context) with no Job in the context gets a fresh root Job() not linked to any parent — nothing cancels or awaits it, so it leaks:

    // Wrong: detached root scope; cleanup depends on a manual cancel() that is easy to miss.
    val scope = CoroutineScope(someContextElement.asContextElement())
    
    // Right: child of an owning scope; cancelled structurally with its parent.
    val scope = owner.coroutineScope.childScope("MyFeature", someContextElement.asContextElement())
    
  • If a manual scope is unavoidable, either make it a child of an owning scope, or register the owning Disposable (cancel the scope in dispose()) before launching any work, and verify that registration actually runs. Disposal-only cancellation with no parent link is fragile — a missed dispose() leaks everything the scope launched.

  • launch in init {} — depends on what cancels the scope. If the object owns a scope tied to its own Disposer registration, don't launch in init: the coroutine can run (touching a half-built this) before registration completes and leak on failure — expose start() and call it after Disposer.register. A @Service with an injected CoroutineScope is the exception: that scope is cancelled with the service's container (app/project/plugin) by the platform — not via your dispose(), and no Disposable is needed solely to cancel the injected scope — so a service self-starting workers from init is acceptable. Just keep the constructor cheap and don't read not-yet-initialized state.

    // Owned scope, cancelled via your own Disposer registration:
    init { scope.launch { observe() } }          // Wrong — may run before Disposer.register completes
    fun start() { scope.launch { observe() } }   // Right — after Disposer.register(owner, this)
    // @Service(private val scope: CoroutineScope) — self-start from init is fine; scope is platform-managed.
    
  • Prefer the shortest lifecycle that consumes the work; use an app/project service scope only when the result is genuinely owned for that whole lifetime.

  • Do not store per-submission UI context in a long-lived scope. Context elements such as current ModalityState must be captured at the submit site, not at service/object construction. Avoid CoroutineScope(ModalityState.defaultModalityState().asContextElement()); prefer scope.launch(currentModality.asContextElement()) { ... } when modality belongs to that operation. Modality dispatch semantics belong to kotlin-ui-swing-component-architecture / platform-deep-dives.

Job hierarchy and failure propagation

  • Cancelling a parent cancels all children recursively; a child failing with anything other than CancellationException cancels its parent and siblings. SupervisorJob/supervisorScope opts child failure out of cancelling the parent; parent cancellation still cancels supervised children.
  • A parent that finished its block is completing, not done, until every child completes.
  • coroutineScope { } is all-or-nothing (one failure fails the scope and rethrows); supervisorScope { } is only for genuinely independent children.

Cooperative cancellation

  • A loop/CPU-bound body with no suspension point is not cancellable and hangs on cancel(). Add a suspension point (delay, yield), ensureActive(), or checkCanceled() in progress-aware code.
  • Callback APIs: bridge with suspendCancellableCoroutine (not suspendCoroutine) and release the resource in invokeOnCancellation.
  • Blocking APIs: use a blocking-appropriate context (Dispatchers.IO) or, for progress-aware blocking code, coroutineToIndicator { indicator -> ... }. Pair cancellation with an explicit interrupt/close strategy; coroutine cancellation alone does not abort an in-progress blocking call. Do not wrap blocking code in blockingContext: deprecated since 2024.2 because context is installed implicitly (ReplaceWith("action()")).

Exception and failure propagation

  • runCatching { launch { ... } } does not catch the child's failure. The child fails its parent scope out-of-band while launch returns normally, so try/runCatching sees nothing. To isolate a child, use supervisorScope and handle the failure inside the child.

  • The same trap applies to async: try/runCatching around async { } does not contain a later failure — the exception surfaces at await(), and an unsupervised failure may already have cancelled the parent before you await.

  • Never swallow cancellation. runCatching and bare catch (e: Throwable) catch CancellationException too; swallowing it breaks structured concurrency and can hang cancellation. ProcessCanceledException is a CancellationException subtype, so catching cancellation covers it too. Rethrow cancellation first:

    try {
      body()
    } catch (c: CancellationException) {   // also covers ProcessCanceledException (a subtype)
      throw c                              // never swallow cancellation
    } catch (x: Throwable) {
      handle(x)
    }
    
  • When bridging to a non-coroutine promise/callback, settle it before rethrowing cancellation. If a coroutine owns an AsyncPromise or a callback result, cancellation can skip the normal result path and leave external awaiters pending. In catch (c: CancellationException), complete the external primitive (setError(c) / cancel) before throw c. Plain Deferred does not need this — cancellation completes it.

  • Do not report a caught CancellationException as a user-visible error, even if you rethrow it.

Don't cancel yourself

Do not call cancel() on the coroutine you are currently running in — it does not stop execution immediately (only at the next suspension point) and poisons the surrounding scope. Return early or throw CancellationException. A shared suspend fun must never cancel its caller's job.

invokeOnCompletion

Prefer to avoid it. Three failure modes make it a trap:

  1. Runs concurrently, unordered, on an unspecified thread — not guaranteed on the EDT or after your surrounding code. A check-then-act on a shared field races; use AtomicReference.compareAndSet.
  2. Retained until the job completes — registering on a long-lived job (or in a loop) accumulates handlers and leaks. Only register on short-lived jobs that actually finish.
  3. Must not throw — exceptions from handlers are reported through coroutine exception handling/logging, not to the caller waiting for completion. Keep the body to trivial, non-failing bookkeeping.

Preferred alternative — do completion work as the last step inside the coroutine:

scope.launch {
  try { doWork() }
  finally { withContext(NonCancellable + Dispatchers.EDT) { onFinished() } }
}

Use NonCancellable only for small, bounded cleanup that must run even after cancellation — never wrap substantial work in it, and keep UI cleanup lifecycle/disposal-guarded (do not touch a disposed component). Cancelling an already-completed Job is a no-op, so "clear my job handle on completion" bookkeeping is often unnecessary — verify it changes behavior before adding it.

Severity defaults

Adjust for actual impact.

PatternDefaultSmallest fix
GlobalScope for feature workCriticalchild of a lifecycle scope
Detached CoroutineScope never cancelledCriticalchildScope of an owning scope
Swallowed CancellationException/ProcessCanceledExceptionCriticalrethrow cancellation before catch (Throwable)
Non-cooperative infinite/long loopCriticaladd ensureActive()/yield/suspension point
runCatching/try around launch/async to contain failureCriticalsupervisorScope + handle inside child
launch in init with a scope tied to own Disposer registrationMajorstart() after registration
Manual scope where childScope fits; scope not tied to lifecycleMajorinject/childScope; register Disposable before launch
invokeOnCompletion mutating shared state unsynchronized or that can throwMajorcompareAndSet, or clean up in coroutine finally
Self-cancellation (cancel() on current job)Majorreturn / throw CancellationException
Promise/callback bridge left pending on cancellationMajorcomplete/cancel external primitive before rethrow
Unnecessary invokeOnCompletion bookkeepingMinorremove it
blockingContext wrapper (deprecated 2024.2; context now implicit)Minordelete the wrapper
Cancellation reported as a user-visible errorMinorrethrow silently
supervisorScope/SupervisorJob where a plain scope sufficesMinoruse coroutineScope

Further reading

For deep internals see platform-deep-dives (coroutine notebooks: cancellation model, context propagation) and the ultimate coroutine docs docs/IntelliJ-Platform/4_man/Kotlin-Coroutines/, especially 9_Gotchas-and-practices/ and 8_UI-EDT-Dispatchers.md.

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

actions

免费有简介

实现或修改 IntelliJ `AnAction`,并在 `plugin.xml` 中注册。将显示名称和说明写入消息资源文件,按需设置图标,并通过事件协程作用域启动挂起任务,避免同步处理阻塞 IDE。

  • 为 IDE 菜单添加操作
  • 修改操作注册或图标
  • 整理操作名称和说明
JetBrains/intellij-community2.1万2026年10月29日 更新

bazel-test-migration

免费有简介

将 IntelliJ JPS 测试模块迁移到 Bazel,并排查仅在 Bazel 中出现的失败。根据症状检查依赖、插件、测试数据和 runfiles,重新生成并验证目标。

  • 将 JPS 测试模块迁移到 Bazel
  • 排查仅在 Bazel 中失败的测试
  • 检查测试依赖和数据配置
JetBrains/intellij-community2.1万2026年10月29日 更新

code-style

免费有简介

按 IntelliJ 项目规范编写或审查 Kotlin 和 Java 代码,涵盖语言选择、格式、密封类型、线程注解及控制流异常处理。

  • 按 IntelliJ 规范编写 Kotlin
  • 审查 Java 代码
  • 检查异常处理和密封类型分支
JetBrains/intellij-community2.1万2026年10月29日 更新

commits

免费有简介

本指南帮助您按 IntelliJ 项目规则拆分更改并撰写提交信息:有工单时使用编号和变更说明,无工单时使用标签与子系统名,并在需要时说明原因和决定。

  • 按项目格式编写带工单号的提交信息
  • 整理测试或文档提交并选择合适标签
  • 按规则修改已有提交信息
JetBrains/intellij-community2.1万2026年10月29日 更新

使用多个 Python 类型检查器比较代码片段或文件的类型推断和报错判断,并汇总各工具的结论、耗时与诊断信息,可保存为 Markdown 报告。

  • 比较不同检查器的类型推断
  • 确认代码是否会被判为类型错误
  • 查看多个工具的诊断差异
JetBrains/intellij-community2.1万2026年10月29日 更新

整理 Compose Desktop 界面中的状态和副作用,让可组合函数负责绘制,由展示器或协调器处理应用决策。了解状态归属、Effect、可复用组件接口、延迟列表键、动画和性能,适用于单项界面修改。

  • 分离界面绘制与展示器决策
  • 整理界面状态的归属
  • 检查 Effect 键和协程用法
JetBrains/intellij-community2.1万2026年10月29日 更新

查看 JetBrains 的全部技能

报告此技能的问题