diff --git a/ijs.tree b/ijs.tree index fad004981..99888064b 100644 --- a/ijs.tree +++ b/ijs.tree @@ -73,7 +73,7 @@ - + diff --git a/topics/appendix/glossary.md b/topics/appendix/glossary.md index 1d4690ba5..c1dcf3101 100644 --- a/topics/appendix/glossary.md +++ b/topics/appendix/glossary.md @@ -161,13 +161,13 @@ Write Action _(WA)_ Write Allowing Read Action _(WARA)_ : A coroutine → _Read Action_ that is canceled by an incoming → _Write Action_. -See [](coroutine_read_actions.md#coroutine-read-actions-api) for details. +See [](coroutine_read_actions.topic#coroutine-read-actions-api) for details. → _Suspending Context_ → _Coroutine_ Write Blocking Read Action _(WBRA)_ : A coroutine → _Read Action_ that blocks incoming → _Write Action_. -See [](coroutine_read_actions.md#coroutine-read-actions-api) for details. +See [](coroutine_read_actions.topic#coroutine-read-actions-api) for details. → _Suspending Context_ → _Coroutine_ diff --git a/topics/basics/architectural_overview/coroutines/coroutine_read_actions.topic b/topics/basics/architectural_overview/coroutines/coroutine_read_actions.topic new file mode 100644 index 000000000..f400d6675 --- /dev/null +++ b/topics/basics/architectural_overview/coroutines/coroutine_read_actions.topic @@ -0,0 +1,255 @@ + + + + + + + + Executing read actions in coroutines. + + + +

+ The concept of read/write locks and running blocking and cancellable read actions is explained in + the Threading section: +

+ +
  • Read-Write Lock
  • +
  • Read Action Cancellability
  • +
    + +

    This section explains running read actions (RA) in coroutines specifically.

    + + +

    + Running RA from coroutines is executed with *ReadAction* functions from + + coroutines.kt + + (see their KDocs for the details). + Functions can be divided into two groups, which differ in reacting to an incoming write action (WA): +

    + + + + + + + + + + + + + + + + + + +
    Write Allowing Read Action (WARA)Write Blocking Read Action (WBRA)
    readActionreadActionBlocking
    smartReadActionsmartReadActionBlocking
    constrainedReadActionconstrainedReadActionBlocking
    + +

    WARA is canceled when a parent coroutine is canceled or a WA arrives.

    + +

    + WBRA is canceled only when a parent coroutine is canceled. + It blocks WA until finishing its lambda. +

    + + +

    + It is important to note that in the coroutines context, default functions + (without the Blocking suffix) behavior prioritizes WA. +

    +

    + In contrast, in the non-coroutine context, + + Application.runReadAction + + and similar methods (without any prefix/suffix) perform RA blocking WA, whereas RA allowing WA are invoked via + the NonBlockingReadAction API. +

    +

    Be careful when migrating the code running read actions to coroutines.

    +
    + + +

    + WARA API is simpler than NonBlockingReadAction (NBRA). + WARA doesn't need the following API methods: +

    + + +
  • + submit(Executor backgroundThreadExecutor) because this is a responsibility of the coroutine + dispatcher +
  • +
  • + executeSynchronously() because effectively they are executed in the current coroutine dispatcher + already +
  • +
  • + expireWhen(BooleanSupplier expireCondition), + expireWith(Disposable parentDisposable), + and wrapProgress(ProgressIndicator progressIndicator) because they are canceled when the calling + coroutine is canceled +
  • +
  • +

    + finishOnUiThread() because this is handled by switching to the + EDT dispatcher. + Note that the UI data must be pure (e.g., strings/icons/element pointers), which inherently cannot + be invalidated during the transfer from a background thread to EDT. +

    +

    + In the case of using NBRA's finishOnUiThread to start a write action, the coroutine equivalent + is readAndWriteAction: +

    + + readAndWriteAction { + val computedData = computeDataInReadAction() + writeAction { + applyData(computedData) + } + } + +

    + It provides the same guarantees as finishOnUIThread + (no WA between computeDataInReadAction and applyData), + but it is not bound to EDT. +

    +
  • +
  • +

    + coalesceBy(Object ... equality) because this should be handled by + + Flow.collectLatest() + + and/or + + Flow.distinctUntilChanged() + . + Usually, NBRAs are run as a reaction to user actions, and there might be multiple NBRAs running, even + if their results are unused. + Instead of cancelling the read action, in the coroutine world the coroutines are canceled: +

    + + eventFlow.collectLatest { event -> + // the next emitted event will cancel the current coroutine + // and run it again with the next event + readAction { readData() } + } + + eventFlow.distinctUntilChanged().collectLatest { event -> + // the next emitted event will cancel the current coroutine + // and run it again with the next event if the next event + // was not equal to the previous one + readAction { readData() } + } + +
  • +
    + + +

    Suspending read actions use coroutines as the underlying framework.

    + +

    + WARA (invoked with mentioned *ReadAction functions) + may make several attempts to execute its lambda. + The block needs to know whether the current attempt was canceled. + *ReadAction functions create a child + + Job + + for each attempt, and this job becomes canceled when a write action arrives. + *ReadAction restarts the block if it was canceled by a write action, or throws + CancellationException if the calling coroutine was canceled, causing the cancellation + of the child Job. +

    + +

    + To check whether the current action was canceled, clients must call ProgressManager.checkCanceled(), which was adjusted to work in coroutines. + Clients must not throw ProcessCanceledException manually. +

    +
    + +
    + +
    + + + + +

    + Read actions must be short. + Technically, it is possible to allow suspension during the read action, but it is complex to implement, + and it still might be surprising: +

    + + + readAction { + withContext(IO) { + // this will be canceled and restarted on each write action + loadTenGigabytesOfIndexes() + } + } + + +

    Also, it is impossible to solve this with a continuation interceptor like:

    + + object ReadAction : ContinuationInterceptor, CoroutineContext.Key<RA> { + override val key: CoroutineContext.Key<*> get() = this + override fun <T> interceptContinuation( + continuation: Continuation<T> + ): Continuation<T> { + return Continuation(continuation.context) { result -> + ApplicationManager.getApplication().runReadAction { + continuation.resumeWith(result) + } + } + } + } + + +

    + It's impossible to give it suspending semantics: the interceptor will block its thread waiting for + the read lock. + The interceptors should not be used for that. +

    + +

    + As of Kotlin 1.5.x, it is not possible to combine interceptors and dispatchers. + Only one of them can exist in the context: +

    + + + withContext(ReadAction) { + foo() + withContext(Dispatchers.Default) { // replaces ReadAction in the context + bar() // this will be called outside of read action + } + } + + + +

    Even if that wasn't the case, the following code will work unexpectedly:

    + + + withContext(ReadAction) { + val foo = foo() + yield() // or another function which will suspend + + // At this point 'foo' crossed the boundary between two read actions => + // 'foo' might be invalidated if there was a write action in between. + bar(foo) + } + +
    +
    + + + +