跳转至

线程模型、读写动作与后台任务

IntelliJ Platform 是高度并发的环境。插件代码必须尊重平台线程模型,否则很容易造成 UI 卡顿、死锁、PSI 失效或随机异常。

两类主要线程

线程 作用 注意事项
EDT UI 事件、UI 更新、写入数据模型 必须快;不要做耗时工作
BGT 后台计算、长任务、索引查询 读取模型要使用 Read Action

EDT 只有一个,BGT 可以有多个。耗时操作放到后台,UI 更新回到 EDT。

读写锁

平台数据结构,例如 PSI、VFS、项目模型,不是普通意义上的线程安全对象。访问它们需要通过平台读写锁:

  • Read Action:读取 PSI/VFS/项目模型。
  • Write Action:修改 PSI/VFS/项目模型。
  • Write Intent:平台内部用于协调可能升级为写入的访问。

插件不要手动管理底层锁,应使用平台提供的 Read/Write Action API。

Read Action

val result = ReadAction.compute<String, Throwable> {
    psiFile.text
}

原则:

  • 后台线程读取 PSI 时必须包 Read Action。
  • Read Action 包裹范围越小越好。
  • 长时间读操作考虑 Non-blocking Read Action,避免阻塞写入和 UI。
  • 两次 Read Action 之间对象可能失效,要检查 isValid

Write Action

WriteCommandAction.runWriteCommandAction(project) {
    psiElement.replace(newElement)
}

原则:

  • 修改 PSI、VFS、项目模型必须使用 Write Action。
  • 用户可撤销的代码修改优先用 WriteCommandAction
  • 不要在 UI renderer 或任意 SwingUtilities.invokeLater() 中修改模型。
  • 对 2023.3+ 平台,涉及模型写入时优先使用 Application.invokeLater() 安排到合适的 modality。

后台任务

耗时任务可以使用 Task.Backgroundable

ProgressManager.getInstance().run(
    object : Task.Backgroundable(project, "Analyzing Project", true) {
        override fun run(indicator: ProgressIndicator) {
            indicator.text = "Collecting files"
            // long-running work
        }
    }
)

建议:

  • 定期检查 indicator.checkCanceled()
  • 不要在后台线程直接更新 Swing UI。
  • 读取 PSI 使用 Read Action。
  • 写入模型拆成短小 Write Action。

Action 线程选择

Action 的 update() 必须声明线程:

override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT

选择规则:

  • 需要读 PSI、VFS、项目模型:BGT
  • 需要访问 Swing UI 状态:EDT
  • update() 不能执行耗时操作。

参考来源