Coroutines on EDT and Locks
Sometimes, errors similar to the below can be observed in the IDE:
Currently, this is not an actual error. Nothing is broken and no data races occurred. This assertion message is a way to prepare plugins for the future changes in the IntelliJ Platform.
Note that this error is shown only when the IDE internal mode is enabled.
Error Context
There are two contexts in which these errors can occur:
Coroutines dispatched on EDT
Consider code that is run on
Dispatchers.EDT
with the following construct (or any of the wrappers for this code):withContext(Dispatchers.EDT) { // ... }Currently, the execution system wraps the executed code into a write intent lock. Such a write intent lock is called implicit.
Specific GUI events
Lock covers all base events (like mouse events, keyboard events, focus change events, etc.), and such lock isn't counted as implicit for now.
But some events can be missed and need personal attention. Especially if such events trigger listeners and the IntelliJ Platform internal message bus.
When code in above contexts checks for the state of lock with ThreadingAssertions.assertReadAccess()
/assertWriteIntentReadAccess()
(or their deprecated Application
counterparts), an error is reported, but the assertion passes.
Only one error for each executed block is reported (see 2. in Planned Changes about a problem with suspending).
Planned Changes
In future releases of the IntelliJ Platform, this implicit lock acquisition will be removed, and warnings will become actual errors (assertions will fail).
The current implicit lock acquisition behavior is not optimal due to the following reasons:
Not every code block executed on EDT needs any lock at all. More acquired locks limit concurrency and degrade performance.
Executed code block could have wrong assumptions, as it can suspend, and at this moment lock will be released and obtained later, on resume. Code looks like it is executed in a single locking session, but in reality it is not.
Required Action
It is preferable to:
minimize locks usage
acquire locks explicitly to make the locked code blocks clear
Code triggering the error should be reviewed and adjusted in a proper way: code which really needs locking should be determined, and proper explicit locking should be used.
Identifying Code Requiring Locking
Each report includes a stacktrace which leads to assertion causing the error.
Wrapping into lock code calling the assertion is seldom the right decision (except some property getters). Often it is necessary to determine which code higher in the stack constitutes a region which should be protected at once. Often (but not always) whole code scheduled to execute on the EDT dispatcher must be protected. Sometimes something in the middle between these extremes.
It is impossible to give one instruction that covers all cases: a proper solution is always context-dependent. The sections below provide some advice on how to find a place requiring locking.
Coroutine Changing Dispatcher Explicitly
A simple case is when in the middle of a coroutine there is withContext(Dispatchers.EDT)
or similar explicit rescheduling on EDT.
In such a case, focus on frames between the assertion call (top frame) and the closets call to invokeSuspend
of any class. Typically, the location of invokeSuspend
(invisible in code) is properly indicated in stacktrace, too.
For example:
In the stacktrace above, a good start for investigation is HttpClientExecutionControllerHelper.kt:158
. Sometimes such frames don't contain proper line information, but the next frame may help to identify the actual location.
In such cases, it is often possible to wrap into lock the whole code scheduled on EDT.
Callbacks (Coroutine and Blocking)
A more complex case is when code is called as callback via coroutines or even as blocking code via some events.
Such cases can be recognized by some generic classes near the top of the stack. Main suspects are:
EventDispatcher
MessageBus
Note that this list is not exhaustive.
Another pattern for this case is the pair of the following calls on the stack, which means default event processing:
Note that this pattern is usable only if there is no invokeSuspend
higher on the stack.
In this case, it is better to find the place where the callback is registered. It may be not so trivial, but searching by usages of some code in stacktrace typically works.
Locking API Recommendations
There are several locking APIs to use for fixing these errors. The following sections list them in the order of preference, from the most recommended to the least.
No API
Sometimes the code in question doesn't need locking and assertion at all.
In some cases, assertion for write intent lock is used instead of assertion for execution on EDT. If it is clear that code doesn't touch PSI or project model or other internal data and if code is under your control, remove the assertion or change it to the proper one.
Suspending readAction()
The readAction()
API is the best and least intrusive. It can suspend by itself but doesn't allow the executed code block to suspend (see 2. in Planned Changes why it is the desired behavior).
One property of readAction()
makes it not as universal as expected: it uses Dispatchers.Default
for code execution. Sometimes it is not desired, as the coroutine in question was scheduled on Dispatchers.EDT
for a reason.
Anyway, it is suspicious when a read action cannot be executed on a background thread and a part of coroutine code cannot be suspended and rescheduled on EDT. This may signal a code issue.
Note that this API cannot be used in the blocking context.
Suspending readAndWriteAction()
A good choice for a coroutine which needs to make some preparation work under a read lock and then complete work under the write lock.
It cannot be used as a drop-in fix - migration to this API requires code reorganization. Consider rewriting code to use it, if possible.
Blocking ReadAction.run()
/compute()
ReadAction.run()
and ReadAction.compute()
are as smart as readAction()
, but they are always executed on the calling thread.
Use them if a read action is required, but it is unacceptable to reschedule code execution on a different dispatcher.
These APIs are marked to use only in the blocking context, so their usage in the suspending context will trigger a warning. It is intentional, as coroutines should be prepared to be rescheduled and should use readAction()
.
Suspending writeIntentReadAction()
and Blocking WriteIntentReadAction.run()
/compute()
These APIs are the last resort. They are intended to be used by the IntelliJ Platform itself. Avoid using them in plugin code.
Use it only as a temporal solution if it is hard to quickly rewrite code which checks for write intent lock.