From 5c9d08a0c2b74156ce3169c7c9916f34c07a61ef Mon Sep 17 00:00:00 2001 From: Konstantin Nisht Date: Mon, 13 Oct 2025 10:57:26 +0200 Subject: [PATCH] [threading] IJPL-178593: Stabilize `Dispatchers.UI` and `backgroundWriteAction` GitOrigin-RevId: a0922de16d4a71f03d0f13be145de707db6f09f7 --- platform/core-api/api-dump-experimental.txt | 5 -- platform/core-api/api-dump-unreviewed.txt | 10 --- platform/core-api/api-dump.txt | 14 +++++ .../openapi/application/coroutines.kt | 63 ++++++++++++------- 4 files changed, 56 insertions(+), 36 deletions(-) diff --git a/platform/core-api/api-dump-experimental.txt b/platform/core-api/api-dump-experimental.txt index d1a31e91efd8..2573a643281f 100644 --- a/platform/core-api/api-dump-experimental.txt +++ b/platform/core-api/api-dump-experimental.txt @@ -210,13 +210,8 @@ com.intellij.openapi.application.Application - *:isWriteThread():Z - *:runWriteIntentReadAction(com.intellij.openapi.util.ThrowableComputable):java.lang.Object f:com.intellij.openapi.application.CoroutinesKt -- *sf:backgroundWriteAction(kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object - *sf:getEdtImmediate(kotlinx.coroutines.Dispatchers):kotlin.coroutines.CoroutineContext -- *sf:getUI(kotlinx.coroutines.Dispatchers):kotlin.coroutines.CoroutineContext - *sf:getUiImmediate(kotlinx.coroutines.Dispatchers):kotlin.coroutines.CoroutineContext -- *sf:getUiWithModelAccess(kotlinx.coroutines.Dispatchers):kotlin.coroutines.CoroutineContext -- *sf:getUiWithModelAccessImmediate(kotlinx.coroutines.Dispatchers):kotlin.coroutines.CoroutineContext -- *sf:readAndBackgroundWriteAction(kotlin.jvm.functions.Function1,kotlin.coroutines.Continuation):java.lang.Object - *sf:readAndBackgroundWriteActionUndispatched(kotlin.jvm.functions.Function1,kotlin.coroutines.Continuation):java.lang.Object - *sf:readAndEdtWriteActionUndispatched(kotlin.jvm.functions.Function1,kotlin.coroutines.Continuation):java.lang.Object - *sf:writeAction(kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object diff --git a/platform/core-api/api-dump-unreviewed.txt b/platform/core-api/api-dump-unreviewed.txt index 3afd6a40cdeb..b6d12c2e0ef6 100644 --- a/platform/core-api/api-dump-unreviewed.txt +++ b/platform/core-api/api-dump-unreviewed.txt @@ -604,17 +604,7 @@ f:com.intellij.openapi.application.ApplicationNamesInfo - getScriptName():java.lang.String f:com.intellij.openapi.application.CoroutinesKt - bsf:asContextElement(com.intellij.openapi.application.ModalityState):kotlin.coroutines.CoroutineContext -- sf:constrainedReadAction(com.intellij.openapi.application.ReadConstraint[],kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object -- sf:constrainedReadActionBlocking(com.intellij.openapi.application.ReadConstraint[],kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object -- sf:constrainedReadAndWriteAction(com.intellij.openapi.application.ReadConstraint[],kotlin.jvm.functions.Function1,kotlin.coroutines.Continuation):java.lang.Object -- sf:edtWriteAction(kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object -- sf:getEDT(kotlinx.coroutines.Dispatchers):kotlin.coroutines.CoroutineContext -- sf:readAction(kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object -- sf:readActionBlocking(kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object -- sf:readAndEdtWriteAction(kotlin.jvm.functions.Function1,kotlin.coroutines.Continuation):java.lang.Object - sf:readAndWriteAction(kotlin.jvm.functions.Function1,kotlin.coroutines.Continuation):java.lang.Object -- sf:smartReadAction(com.intellij.openapi.project.Project,kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object -- sf:smartReadActionBlocking(com.intellij.openapi.project.Project,kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object com.intellij.openapi.application.DumbAwareSearchParameters - com.intellij.util.QueryParameters - a:getProject():com.intellij.openapi.project.Project diff --git a/platform/core-api/api-dump.txt b/platform/core-api/api-dump.txt index 87a0a831cd7a..0728aaa4bf52 100644 --- a/platform/core-api/api-dump.txt +++ b/platform/core-api/api-dump.txt @@ -449,6 +449,20 @@ com.intellij.openapi.application.BaseExpirableExecutor - a:expireWith(com.intellij.openapi.Disposable):com.intellij.openapi.application.BaseExpirableExecutor - a:submit(java.lang.Runnable):org.jetbrains.concurrency.CancellablePromise - a:submit(java.util.concurrent.Callable):org.jetbrains.concurrency.CancellablePromise +f:com.intellij.openapi.application.CoroutinesKt +- sf:backgroundWriteAction(kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object +- sf:constrainedReadAction(com.intellij.openapi.application.ReadConstraint[],kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object +- sf:constrainedReadActionBlocking(com.intellij.openapi.application.ReadConstraint[],kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object +- sf:constrainedReadAndWriteAction(com.intellij.openapi.application.ReadConstraint[],kotlin.jvm.functions.Function1,kotlin.coroutines.Continuation):java.lang.Object +- sf:edtWriteAction(kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object +- sf:getEDT(kotlinx.coroutines.Dispatchers):kotlin.coroutines.CoroutineContext +- sf:getUI(kotlinx.coroutines.Dispatchers):kotlin.coroutines.CoroutineContext +- sf:readAction(kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object +- sf:readActionBlocking(kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object +- sf:readAndBackgroundWriteAction(kotlin.jvm.functions.Function1,kotlin.coroutines.Continuation):java.lang.Object +- sf:readAndEdtWriteAction(kotlin.jvm.functions.Function1,kotlin.coroutines.Continuation):java.lang.Object +- sf:smartReadAction(com.intellij.openapi.project.Project,kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object +- sf:smartReadActionBlocking(com.intellij.openapi.project.Project,kotlin.jvm.functions.Function0,kotlin.coroutines.Continuation):java.lang.Object com.intellij.openapi.application.ModalityStateListener - java.util.EventListener - beforeModalityStateChanged(Z,java.lang.Object):V diff --git a/platform/core-api/src/com/intellij/openapi/application/coroutines.kt b/platform/core-api/src/com/intellij/openapi/application/coroutines.kt index 2d0d8f5704d1..51aa2a0e8b99 100644 --- a/platform/core-api/src/com/intellij/openapi/application/coroutines.kt +++ b/platform/core-api/src/com/intellij/openapi/application/coroutines.kt @@ -161,7 +161,6 @@ suspend fun readAndWriteAction(action: ReadAndWriteScope.() -> ReadResult /** * Same as [readAndEdtWriteAction], but invokes write actions on a background thread instead of EDT. */ -@Experimental suspend fun readAndBackgroundWriteAction(action: ReadAndWriteScope.() -> ReadResult): T { return readWriteActionSupport().executeReadAndWriteAction(emptyArray(), false, false, action) } @@ -293,19 +292,13 @@ suspend fun writeAction(action: () -> T): T { * Runs given [action] under [write lock][com.intellij.openapi.application.Application.runWriteAction]. * * This function dispatches the [action] by [Dispatchers.Default] within the [context modality state][asContextElement]. - * Acquiring the write-lock happens in blocking manner, - * i.e. [runWriteAction][com.intellij.openapi.application.Application.runWriteAction] call will block - * until all currently running read actions are finished. + * The lock is acquired in suspending manner, so the calling coroutine will be suspended during the acquisition. * - * NB This function is an API stub. - * The implementation will change once running write actions would be allowed on other threads. - * This function exists to make it possible to use it in suspending contexts - * before the platform is ready to handle write actions differently. + * A pending background write action can be diagnosed by an inspection of _coroutine dumps_. * * @see readAndBackgroundWriteAction * @see com.intellij.openapi.command.writeCommandAction */ -@Experimental suspend fun backgroundWriteAction(action: () -> T): T { return readWriteActionSupport().runWriteAction(action) } @@ -343,12 +336,27 @@ private fun readWriteActionSupport() = ApplicationManager.getApplication().getSe fun ModalityState.asContextElement(): CoroutineContext = asContextElement() /** - * UI dispatcher which dispatches onto Swing event dispatching thread within the [context modality state][asContextElement]. - * The computations scheduled by this dispatcher **are protected** by the Write-Intent lock, and they are allowed to upgrade to write actions. + * A coroutine dispatcher that executes coroutines on the AWT Event Dispatching Thread + * within the [context modality state][asContextElement] and with write-intent lock acquired. + * + * Use-cases: + * - Use this dispatcher if you are working with the legacy IntelliJ Platform model code (PSI, VFS, Documents) which requires model access on EDT. + * - Use [Dispatchers.UI] if you are working with the UI and you do not touch the IntelliJ Platform model. + * + * ### Locking Behavior + * This dispatcher is different from [Dispatchers.UI] in the aspect of handling the Read/Write lock: + * the computations scheduled by this dispatcher **are protected by the Write-Intent lock** (see [Application]), + * hence they are allowed to acquire write actions atomically. + * + * ### Ordering Guarantees + * This dispatcher is fair: two `launch(Dispatchers.EDT)` are executed in the order of their scheduling. + * + * _NB:_ There are no ordering guarantees between `launch(Dispatchers.EDT)` and `launch(Dispatchers.UI)` coroutines: + * when scheduled sequentially, either one can execute first. + * + * ### Modality Behavior * * If no context modality state is specified, then the coroutine is dispatched within [ModalityState.nonModal] modality state. - * - * Prefer [Dispatchers.UI] for computations on EDT. */ @Suppress("UnusedReceiverParameter") val Dispatchers.EDT: CoroutineContext get() = coroutineSupport().uiDispatcher(UiDispatcherKind.LEGACY, false) @@ -367,15 +375,28 @@ val Dispatchers.EDT: CoroutineContext get() = coroutineSupport().uiDispatcher(Ui fun Dispatchers.ui(kind: UiDispatcherKind = UiDispatcherKind.STRICT, immediate: Boolean = false): CoroutineContext = coroutineSupport().uiDispatcher(kind, immediate) /** - * UI dispatcher which dispatches onto Swing event dispatching thread within the [context modality state][asContextElement]. - * The computations scheduled by this dispatcher are **not** protected by any lock, and it is forbidden to initiate Read or Write actions. + * A coroutine dispatcher that executes coroutines on the AWT Event Dispatching Thread + * within the [context modality state][asContextElement] and without write-intent lock acquired. + * + * Use-cases: + * - Use this dispatcher if you are working with the UI and you do not touch the IntelliJ Platform model. + * - Use [Dispatchers.EDT] if you are working with the IntelliJ Platform model on EDT. + * + * ### Locking Behavior + * This dispatcher is different from [Dispatchers.EDT] in the aspect of handling the Read/Write lock: + * the computations scheduled by this dispatcher **are not protected by the Write-Intent lock** (see [Application]), + * and it is forbidden to initiate read or write actions inside. + * + * ### Ordering Guarantees + * This dispatcher is fair: two `launch(Dispatchers.UI)` are executed in the order of their scheduling. + * + * _NB:_ There are no ordering guarantees between `launch(Dispatchers.EDT)` and `launch(Dispatchers.UI)` coroutines: + * when scheduled sequentially, either one can execute first. + * + * ### Modality Behavior * * If no context modality state is specified, then the coroutine is dispatched within [ModalityState.nonModal] modality state. - * - * Use [Dispatchers.UI] when in doubt, use [Dispatchers.Main] if the coroutine doesn't care about IntelliJ Platform model (PSI, VFS, etc.), - * e.g., when it can be executed outside of IJ process. */ -@get:Experimental @Suppress("UnusedReceiverParameter") val Dispatchers.UI: CoroutineContext get() = coroutineSupport().uiDispatcher(kind = UiDispatcherKind.STRICT, immediate = false) @@ -385,7 +406,7 @@ val Dispatchers.UI: CoroutineContext get() = coroutineSupport().uiDispatcher(kin * * If no context modality state is specified, then the coroutine is dispatched within [ModalityState.nonModal] modality state. */ -@get:Experimental +@get:Internal @Suppress("UnusedReceiverParameter") val Dispatchers.UiWithModelAccess: CoroutineContext get() = coroutineSupport().uiDispatcher(kind = UiDispatcherKind.RELAX, immediate = false) @@ -407,7 +428,7 @@ val Dispatchers.UiImmediate: CoroutineContext get() = coroutineSupport().uiDispa * The version of [Dispatchers.UiWithModelAccess] which has properties of [MainCoroutineDispatcher.immediate] */ @Suppress("UnusedReceiverParameter") -@get:Experimental +@get:Internal val Dispatchers.UiWithModelAccessImmediate: CoroutineContext get() = coroutineSupport().uiDispatcher(kind = UiDispatcherKind.RELAX, immediate = true) private fun coroutineSupport() = ApplicationManager.getApplication().getService(CoroutineSupport::class.java)