IntelliJ Platform Plugin SDK Help

通用线程规则

读-写锁

通常, IntelliJ Platform 中与代码相关的数据结构由单个读取器/写入器锁管理.

你不能在读写Action之外读取下列子系统中中的模型:

任何线程都可以 读取 数据. 从UI线程读取数据不需要任何特殊的努力. 但是,从任何其他线程执行的读取操作需要使用ApplicationManager.getApplication().runReadAction()或更短的ReadAction run()/compute()包装在读取操作中.

对应的对象不会保证在多个并行读取操作中存活. 作为一个经验法则, 当启动读取操作时, 检查 PSI/VFS/项目/模块 对象是否还有效.

只允许从UI线程写入数据,并且写操作总是需要用ApplicationManager.getApplication().runWriteAction()或更短的WriteAction run()/compute()包装在一个写操作中.

此外,只允许从写安全上下文修改模型,其中包括用户操作,来自它们的invokeLater调用(参见下一节). 您不能从UI渲染器或SwingUtilities.invokeLater()调用中修改PSI,VFS或项目模型.

模态和 invokeLater()

To pass control from a background thread to the Event Dispatch Thread (EDT), instead of the standard SwingUtilities.invokeLater(), plugins should use ApplicationManager.getApplication().invokeLater(). The latter API allows specifying the modality state (ModalityState) for the call, i.e., the stack of modal dialogs under which the call is allowed to execute:

ModalityState.NON_MODAL

The operation will be executed after all modal dialogs are closed. If any of the open (unrelated) projects displays a per-project modal dialog, the action will be performed after the dialog is closed.

ModalityState.stateForComponent()

The operation can be executed when the topmost shown dialog is the one that contains the specified component or is one of its parent dialogs.

None Specified

ModalityState.defaultModalityState() will be used. This is the optimal choice in most cases that uses the current modality state when invoked from UI thread. It has special handling for background processes started with ProgressManager: invokeLater() from such a process may run in the same dialog that the process started.

ModalityState.any()

The operation will be executed as soon as possible regardless of modal dialogs. Please note that modifying PSI, VFS, or project model is prohibited from such runnables.

If a UI thread activity needs to access file-based index (e.g., it's doing any project-wide PSI analysis, resolves references, etc.), please use DumbService.smartInvokeLater(). That way, it is run after all possible indexing processes have been completed.

Background Processes and ProcessCanceledException

Background progresses are managed by ProgressManager class, which has plenty of methods to execute the given code with a modal (dialog), non-modal (visible in the status bar), or invisible progress. In all cases, the code is executed on a background thread, which is associated with a ProgressIndicator object. The current thread's indicator can be retrieved any time via ProgressIndicatorProvider.getGlobalProgressIndicator().

For visible progresses, threads can use ProgressIndicator to notify the user about current status: e.g., set text or visual fraction of the work done.

Progress indicators also provide means to handle cancellation of background processes, either by the user (pressing the Cancel button) or from code (e.g., when the current operation becomes obsolete due to some changes in the project). The progress can be marked as canceled by calling ProgressIndicator.cancel(). The process reacts to this by calling ProgressIndicator.checkCanceled() (or ProgressManager.checkCanceled() if no indicator instance at hand). This call throws a special unchecked ProcessCanceledException if the background process has been canceled.

All code working with PSI, or in other kinds of background processes, must be prepared for ProcessCanceledException being thrown from any point. This exception should never be logged but rethrown, and it'll be handled in the infrastructure that started the process.

The checkCanceled() should be called often enough to guarantee the process's smooth cancellation. PSI internals have a lot of checkCanceled() calls inside. If a process does lengthy non-PSI activity, insert explicit checkCanceled() calls so that it happens frequently, e.g., on each Nth loop iteration.

Read Action Cancellability

Background threads shouldn't take plain read actions for a long time. The reason is that if the UI thread needs a write action (e.g., the user types something), it must be acquired as soon as possible. Otherwise, the UI will freeze until all background threads have released their read actions.

The best-known approach is to cancel background read actions whenever there's a write action about to occur, and restart that background read action later from scratch. Editor highlighting, code completion, Goto Class/File/... actions all work like this.

To achieve that, the lengthy background operation is started with a ProgressIndicator, and a dedicated listener cancels that indicator when write action is initiated. The next time the background thread calls checkCanceled(), a ProcessCanceledException is thrown, and the thread should stop its operation (and finish the read action) as soon as possible.

There are two recommended ways of doing this:

  • If on UI thread, call ReadAction.nonBlocking() which returns NonBlockingReadAction

  • If already in a background thread, use ProgressManager.getInstance().runInReadActionWithWriteActionPriority() in a loop, until it passes or the whole activity becomes obsolete.

In both approaches, always check at the start of each read action, if the objects are still valid, and if the whole operation still makes sense (i.e., not canceled by the user, the project isn't closed, etc.). With ReadAction.nonBlocking(), use expireWith() or expireWhen() for that.

If the activity has to access file-based index (e.g., it's doing any project-wide PSI analysis, resolves references, etc.), use ReadAction.nonBlocking(...).inSmartMode().

Avoiding UI Freezes

Do not Perform Long Operations in UI Thread

In particular, don't traverse Virtual File System, parse PSI, resolve references or query indexes/stubs.

There are still some cases when the platform itself invokes such expensive code (e.g., resolve in AnAction.update()), but these are being worked on. Meanwhile, please try to speed up what you can in your plugin, it'll be beneficial anyway, as it'll also improve background highlighting performance.

WriteActions currently have to happen on UI thread, so to speed them up, you can try moving as much as possible out of write action into a preparation step which can be then invoked in background (e.g., using ReadAction.nonBlocking(), see above).

Don't do anything expensive inside event listeners. Ideally, you should only clear some caches. You can also schedule background processing of events, but be prepared that some new events might be delivered before your background processing starts, and thus the world might have changed by that moment or even in the middle of background processing. Consider using MergingUpdateQueue and ReadAction.nonBlocking() to mitigate these issues.

Massive batches of VFS events can be pre-processed in background, see AsyncFileListener (2019.2 or later).

Last modified: 29 九月 2022