diff --git a/docs/notebooks/3-ReadWriteLock.ipynb b/docs/notebooks/3-ReadWriteLock.ipynb new file mode 100644 index 000000000000..15870281d818 --- /dev/null +++ b/docs/notebooks/3-ReadWriteLock.ipynb @@ -0,0 +1,1555 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": "# The Read-Write Lock" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T09:24:15.091288Z", + "start_time": "2025-08-06T09:24:15.020490Z" + } + }, + "cell_type": "code", + "source": [ + "%use intellij-platform\n", + "import com.intellij.openapi.application.ApplicationManager\n", + "import com.intellij.util.application\n", + "import kotlinx.coroutines.runBlocking\n", + "import com.intellij.openapi.application.EDT\n", + "import com.intellij.openapi.application.runReadAction\n", + "import com.intellij.openapi.application.runWriteAction\n", + "import kotlinx.coroutines.Dispatchers\n", + "import kotlinx.coroutines.delay\n", + "import kotlinx.coroutines.launch\n", + "import com.intellij.openapi.application.ReadAction\n", + "import com.intellij.openapi.application.writeAction\n", + "import com.intellij.openapi.progress.ProgressManager\n", + "import kotlinx.coroutines.GlobalScope\n", + "import kotlinx.coroutines.asExecutor\n", + "import java.util.concurrent.Callable\n", + "import com.intellij.openapi.application.readAction\n", + "import com.intellij.openapi.application.edtWriteAction\n", + "import com.intellij.openapi.application.backgroundWriteAction\n", + "import com.intellij.openapi.progress.runBlockingCancellable\n", + "import com.intellij.platform.ide.progress.ModalTaskOwner\n", + "import com.intellij.platform.ide.progress.runWithModalProgressBlocking\n", + "import kotlinx.coroutines.withContext\n", + "import com.intellij.openapi.application.readAndEdtWriteAction\n", + "import java.util.concurrent.atomic.AtomicInteger\n", + "import com.intellij.openapi.progress.ProcessCanceledException\n", + "\n", + "\n", + "\n" + ], + "outputs": [], + "execution_count": 19 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "IntelliJ Platform is a multithreaded application. It also contains a globally accessible set of mutable structures -- such as PSI or a snapshot of the Virtual File System. These structures are not inherently thread-safe, so the Platform must have a way of restricting concurrent access to them. Usually it is done with a mutual exclusion or persistent data structures. In IntelliJ Platform, the approach with a global lock was chosen.\n", + "\n", + "The platform works on top of the Read-Write lock. This kind of lock is a specialization of a regular mutex which allow taking \"read\" or \"write\" permits of access to data. Multiple \"read\" states can coexist, but there can be only one \"write\" state at a time. This specialization ensures that the Platform can perform multiple read operations in parallel.\n", + "\n", + "| Can coexist | READ | WRITE |\n", + "|-------------|------|-------|\n", + "| READ | ✅ | ❌ |\n", + "| WRITE | ❌ | ❌ |" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Platform API" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "The simplest way to run something under read or write lock is to use the functions from `Application`:\n" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T09:28:56.581774Z", + "start_time": "2025-08-06T09:28:56.255146Z" + } + }, + "cell_type": "code", + "source": [ + "application.runReadAction {\n", + " DISPLAY(\"Read access in read action: ${application.isReadAccessAllowed}\")\n", + " DISPLAY(\"Write access in read action: ${application.isWriteAccessAllowed}\")\n", + "}\n", + "// an alias to application.runReadAction\n", + "runReadAction { }\n", + "\n", + "DISPLAY(\"-------------\")\n", + "\n", + "application.invokeLater {\n", + " application.runWriteAction {\n", + " DISPLAY(\"Read access in write action: ${application.isReadAccessAllowed}\")\n", + " DISPLAY(\"Write access in write action: ${application.isWriteAccessAllowed}\")\n", + " }\n", + " // an alias to application.runWriteAction\n", + " runWriteAction {}\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Read access in read action: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Write access in read action: false" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "-------------" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Read access in write action: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Write access in write action: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 29 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "These functions execute the actions synchronously, and they block the underlying thread until the acquisition of the lock is possible. Later we will see how to work with locks in an asynchronus way.\n", + "\n", + "One interesting thing you can notice above is that `runWriteAction` is preceded with `invokeLater`. This is because historically the platform allowed write actions to run only on EDT (the Event Dispatch Thread).\n" + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T10:08:42.748140Z", + "start_time": "2025-08-05T10:08:42.611829Z" + } + }, + "cell_type": "code", + "source": [ + "try {\n", + " application.runWriteAction {}\n", + " error(\"Should not terminate successfully\")\n", + "}\n", + "catch (e: IllegalStateException) {\n", + " println(\"${e.message}\")\n", + "}" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Background write action is not permitted on this thread. Consider using `backgroundWriteAction`, or switch to EDT\n" + ] + } + ], + "execution_count": 3 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\n", + "\n", + "This is a crucial part of the IntelliJ Platform Threading Model: **by default, write actions are allowed only on UI thread**. This behavior has several consequences:\n", + "1. It is relatively easy to implement interactions of the UI and the model. The programmer does not need to think about possible ways of synchronization and consistency -- if only one thread is allowed to draw and change the model, then it is impossible for the UI to see inconsistent states of the model. Moreover, one can perform several model changes and UI modifications in the same frame, which further simplifies the development.\n", + "2. As we are using the UI thread for non-UI tasks, we confine ourselves to the possibility of UI freezes. Since one of the structures that needs to be protected with the lock is VFS, we sometimes do IO operations on EDT, which leads to 1-minute-long freezes.\n", + "\n", + "The second problem is considered significant. There is an ongoing effort on transferring write actions to a background thread." + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## The Write-Intent Lock" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "As part of its stable API, the Platform provides only read and write actions, giving an impression that it works on top of Read/Write lock. In fact, the platform uses Read/Write/Write-Intent lock.\n", + "\n", + "The actions executed under write-intent state are often called _write-intent read_ actions, as they essentially provide read access.\n", + "\n", + "Write-Intent is an additional state of the lock, which does not prevent parallel read access, while allowing atomic upgrade to write:\n", + "\n", + "| Can coexist | READ | WRITE | WRITE-INTENT-READ |\n", + "|-------------------|:------:|:-------:|:-----------------:|\n", + "| READ | ✅ | ❌ | ✅ |\n", + "| WRITE | ❌ | ❌ | ❌ |\n", + "| WRITE-INTENT-READ | ✅ | ❌ | ❌ |\n" + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T10:08:59.511046Z", + "start_time": "2025-08-05T10:08:59.453530Z" + } + }, + "cell_type": "code", + "source": [ + "application.runWriteIntentReadAction {\n", + " DISPLAY(\"Read access allowed: ${application.isReadAccessAllowed}\")\n", + " DISPLAY(\"Write-Intent access allowed: ${application.isWriteIntentLockAcquired}\")\n", + " DISPLAY(\"Write access allowed: ${application.isWriteAccessAllowed}\")\n", + "}\n" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Read access allowed: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Write-Intent access allowed: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Write access allowed: false" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 4 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Write-Intent state emerged during the Platform's initial attempts to run write actions on background.\n", + "\n", + "Imagine that there is an execution inside a single EDT event. This event may execute some read operations, and then modify something in a short write action. When write actions are confined to a single thread, this is not a problem at all -- EDT is the only one who control writes. But if there can be some other write operations, then, to ensure semantical backward compatibility, the model inside this EDT event must be _consistent_ -- the EDT event can execute several read and write actions sequentially, and it does not expect that the model can change between these reads and writes.\n", + "The easiest way to achieve consistency is to take the write lock for each EDT event -- but that would harm scalability, as background read actions would not get a chance to proceed.\n", + "\n", + "So in this case the platform uses write-intent lock -- each EDT event acquired the write-intent lock, and then they can run several read and write operations without worrying about accidental model changes. Write-Intent state prevents simultaneous write actions but allows background read actions." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T10:09:10.934068Z", + "start_time": "2025-08-05T10:09:10.884644Z" + } + }, + "cell_type": "code", + "source": [ + "DISPLAY(\"Write-Intent access allowed by default: ${application.isWriteIntentLockAcquired}\")\n", + "application.invokeLater {\n", + " DISPLAY(\"Write-Intent access allowed in invokeLater: ${application.isWriteIntentLockAcquired}\")\n", + "}\n", + "application.invokeAndWait {\n", + " DISPLAY(\"Write-Intent access allowed in invokeAndWait: ${application.isWriteIntentLockAcquired}\")\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Write-Intent access allowed by default: false" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Write-Intent access allowed in invokeLater: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Write-Intent access allowed in invokeAndWait: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 5 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "\n", + "Write-Intent action is not intended as a public API and eventually will be discouraged to use. They are allowed only on EDT.\n", + "However, sometimes there can be a pattern where you can gather some data in read action and then apply it in write action, with guarantees that the transition between read and write is atomic. For this case, the platform provides `readAndWriteAction` and `NonBlockingReadAction.finishOnUiThread`. More on this below.\n", + "\n", + "**Write-Intent Read action is internal part of the Platform API that helps to ensure consistency of model in UI events.**" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Cancellability of Read Actions" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "A significant part of locking API in the platform is driven by the intention to make the UI thread free and responsive.\n", + "This results in several concepts that are important to understand for perceived performance optimization: pending write actions and non-blocking read actions.\n" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Pending Write Actions" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "A very frequent source of UI freezes is in the acquisition of lock:" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T10:09:16.704977Z", + "start_time": "2025-08-05T10:09:15.517185Z" + } + }, + "cell_type": "code", + "source": [ + "runBlocking {\n", + " launch(Dispatchers.EDT) {\n", + " delay(100)\n", + " runWriteAction { } // a short write action\n", + " }\n", + " launch(Dispatchers.Default) {\n", + " runReadAction {\n", + " Thread.sleep(1000)\n", + " }\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "StandaloneCoroutine{Completed}@5e2e1358" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 6 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "You can notice a UI freeze in your IDE, despite that the write action is almost instantaneous. That's because the EDT is frozen on the acquisition of the write lock. The write lock cannot be acquired because there it waits for read actions to terminate gracefully.\n", + "\n", + "**The Platform's policy is that Write Actions have a higher priority than read actions.** One consequence of this is the state of _pending write actions_.\n", + "\n", + "A write action is _pending_ if it signalled that it wants to acquire a write lock, but not actually started executing an action.\n", + "When a write action is pending, new read actions cannot start. Existing read actions continue to run. If a read action is _non-blocking_, then it gets canceled. When all read actions finish, then write action can start." + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Non-Blocking Read Actions" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "It is often not enough to postpone new read actions when write action is pending. We also need to quickly terminate existing read actions to ensure that a model change gets processed quickly and UI starts handling painting events again.\n", + "For this purpose, the Platform has a concept of _non-blocking read actions_. The naming choice is a bit unfortunate here, as it is not related to blocking of a thread. Treat this name as a separate concept.\n", + "\n", + "Non-blocking read actions have two important properties:\n", + "1. When a write action becomes pending, a non-blocking read action gets _canceled_. You can learn about cancellation more in [the notebook about the Cancellation Model](./2-CancellationModel.ipynb);\n", + "2. If a non-blocking read action was canceled because of a pending write action, it gets **restarted** after write action finishes.\n", + "\n", + "As a consequence, it is important that a non-blocking read action is [_idempotent_](https://en.wikipedia.org/wiki/Idempotence): a runnable inside it can be executed multiple times until it manages to finish without interruptions." + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "The default way of running non-blocking read actions in Java and blocking Kotlin code is with the builder `ReadAction.nonBlocking`:" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T10:18:41.926229Z", + "start_time": "2025-08-05T10:18:40.206394Z" + } + }, + "cell_type": "code", + "source": [ + "import com.intellij.util.io.await\n", + "import java.util.concurrent.atomic.AtomicInteger\n", + "\n", + "runBlocking {\n", + " val counter = AtomicInteger()\n", + " val promise = ReadAction.nonBlocking(Callable {\n", + " DISPLAY(\"Non-blocking read action starts\")\n", + " Thread.sleep(500)\n", + " DISPLAY(\"Non-blocking read action is still working\")\n", + " if (counter.incrementAndGet() < 3) {\n", + " ProgressManager.checkCanceled()\n", + " }\n", + " DISPLAY(\"Non-blocking read action passed cancellation check\")\n", + " })\n", + " .submit(Dispatchers.Default.asExecutor())\n", + "\n", + " delay(100)\n", + " writeAction { }\n", + " promise.await()\n", + "}\n" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Non-blocking read action starts" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Non-blocking read action is still working" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Non-blocking read action starts" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Non-blocking read action is still working" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Non-blocking read action starts" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Non-blocking read action is still working" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Non-blocking read action passed cancellation check" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 19 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Combinations of Read Actions" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Due to the scale of IntelliJ Platform, the Read/Write lock is [reentrant](https://en.wikipedia.org/wiki/Reentrant_mutex):" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T11:16:59.877417Z", + "start_time": "2025-08-05T11:16:59.692485Z" + } + }, + "cell_type": "code", + "source": [ + "runReadAction {\n", + " runReadAction {\n", + " DISPLAY(\"Ok!\")\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Ok!" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 44 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Reentrancy works the following way: if a thread already holds read lock, then the attempt to acquire read lock will proceed directly. In particular, the cancellability of a stack of read actions depends only on the _topmost_ read action." + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T09:25:06.894758Z", + "start_time": "2025-08-06T09:25:06.671979Z" + } + }, + "cell_type": "code", + "source": [ + "// here we wrap a blocking RA into a non-blocking RA\n", + "ReadAction.nonBlocking(Callable { // non-blocking RA\n", + " runReadAction { // blocking RA\n", + " GlobalScope.launch(Dispatchers.EDT) {\n", + " runWriteAction {\n", + " }\n", + " }\n", + " Thread.sleep(10)\n", + " try {\n", + " ProgressManager.checkCanceled()\n", + " }\n", + " catch (e: ProcessCanceledException) {\n", + " DISPLAY(\"Throwing ProcessCanceledException!\")\n", + " throw e\n", + " }\n", + " DISPLAY(\"Ok!\")\n", + " }\n", + "}).submit(Dispatchers.Default.asExecutor()).get()" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Throwing ProcessCanceledException!" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Throwing ProcessCanceledException!" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Throwing ProcessCanceledException!" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Ok!" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 26 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T09:27:44.508681Z", + "start_time": "2025-08-06T09:27:44.320063Z" + } + }, + "cell_type": "code", + "source": [ + "runReadAction { // blocking read action\n", + " ReadAction.nonBlocking(Callable { // non-blocking read action\n", + " GlobalScope.launch(Dispatchers.EDT) {\n", + " runWriteAction {\n", + " }\n", + " }\n", + " Thread.sleep(10)\n", + " try {\n", + " ProgressManager.checkCanceled()\n", + " }\n", + " catch (e: ProcessCanceledException) {\n", + " DISPLAY(\"Throwing ProcessCanceledException!\") // notice, this line is never printed\n", + " throw e\n", + " }\n", + " DISPLAY(\"Ok!\")\n", + " }).executeSynchronously()\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Ok!" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 28 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Performance Tips" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "The biggest source of UI freeze is a blocking read action that prevents a write action on EDT from starting.\n", + "To avoid this, prefer using non-blocking read actions.\n", + "\n", + "In some cases, non-blocking read actions can waste a lot of CPU time on new attempts. In this case, it is advisable to revise your usage pattern and split non-blocking read actions into several smaller ones.\n" + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Read-Write Lock and Coroutines" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "The platform provides coroutine-friendly API for performing actions under read or write lock.\n", + "\n", + "The actions protected by lock are usually _not_ suspending, i.e., the Platform utilities often have this signature:\n", + "```kotlin\n", + "suspend fun lockingAction(action: () -> T): T\n", + "```\n", + "\n", + "There are several reasons behind this choice:\n", + "1. The action under lock needs to be fast -- otherwise it will worsen parallelism of the application (if we allow suspending write action) or hinder responsiveness (in case of suspending read action).\n", + "2. Most IDE models are not prepared for concurrent modification. To ensure safety, we will not provide `suspend` modifier inside a write action." + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### `readAction`" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "The default choice for initiating read actions in coroutine context is `readAction`. This is a **non-blocking** read action which suspends on lock acquisition." + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T10:22:50.699554Z", + "start_time": "2025-08-05T10:22:50.627434Z" + } + }, + "cell_type": "code", + "source": [ + "import com.intellij.openapi.progress.ProcessCanceledException\n", + "\n", + "runBlocking(Dispatchers.Default) {\n", + " repeat(5) { counter ->\n", + " launch {\n", + " edtWriteAction {\n", + " DISPLAY(\"Write action $counter is executed\")\n", + " }\n", + " }\n", + " }\n", + " readAction {\n", + " DISPLAY(\"Read action starts\")\n", + " try {\n", + " ProgressManager.checkCanceled()\n", + " }\n", + " catch (e: ProcessCanceledException) {\n", + " DISPLAY(\"Read action cancelled\")\n", + " throw e\n", + " }\n", + " DISPLAY(\"Read action passed cancellation check\")\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Read action starts" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Read action cancelled" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Write action 0 is executed" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Write action 2 is executed" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Read action starts" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Read action cancelled" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Write action 4 is executed" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Read action starts" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Read action cancelled" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Write action 1 is executed" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Write action 3 is executed" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Read action starts" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Read action passed cancellation check" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 28 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### `edtWriteAction`" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "To run write action in suspending code, consider using `edtWriteAction`. This function will switch to EDT and run a write action there. Its name contrasts to `backgroundWriteAction`, which is currently in experimental stage." + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T10:09:54.888109Z", + "start_time": "2025-08-05T10:09:54.789212Z" + } + }, + "cell_type": "code", + "source": [ + "GlobalScope.launch(Dispatchers.Default) {\n", + " edtWriteAction {\n", + " DISPLAY(\"Write access: ${application.isWriteAccessAllowed}\")\n", + " DISPLAY(\"EDT: ${application.isDispatchThread}\")\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Write access: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "EDT: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "StandaloneCoroutine{Completed}@1b4a2ffe" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 8 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### `readAndWriteAction`" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Sometimes there is a need to perform a possibly long operation of collecting some data in read action, and then apply the collected data in write action. One cannot simply use consecutive `readAction` and `edtWriteAction` -- the collected data may become inconsistent if some other `edtWriteAction` appears in the gap between these two functions. So to perform this operation, we need an atomic transition between read and write parts.\n", + "It is also not desirable to use `writeIntentReadAction` -- while it does provide atomicity of transition, it will prevent write actions from proceeding.\n", + "\n", + "The Platform provides a function `readAndEdtWriteAction` to support this pattern. Effectively, this utility runs read action part in non-blocking style, and then it checks that the data is still valid at the entrance of the write part." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T09:19:53.091300Z", + "start_time": "2025-08-06T09:19:52.432193Z" + } + }, + "cell_type": "code", + "source": [ + "runBlocking(Dispatchers.Default) {\n", + " val data = AtomicInteger(0)\n", + " repeat(5) {\n", + " launch {\n", + " edtWriteAction {\n", + " data.set(2)\n", + " }\n", + " }\n", + " }\n", + " val restartCounter = AtomicInteger()\n", + " readAndEdtWriteAction {\n", + " restartCounter.incrementAndGet()\n", + " data.set(1)\n", + " writeAction {\n", + " DISPLAY(\"Finished in write action with: ${data}; was restarted $restartCounter times\")\n", + " }\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Finished in write action with: 1; was restarted 2 times" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 18 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Parallel Read Action" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Sometimes there can be a need to perform a suspending operation while holding a read lock. The Platform discourages such situations -- it is better to refactor the code so that it does not perform expensive operations under read lock. Remember, that suspending read actions are non-blocking, so they will rerun the lambda on each cancellation.\n", + "\n", + "Still, there is a pattern that may be helpful. One can use `runBlockingCancellable` while holding read lock, and the suspending computation inside `runBlockingCancellable` shall inherit read access." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T10:33:21.529928Z", + "start_time": "2025-08-05T10:33:21.286913Z" + } + }, + "cell_type": "code", + "source": [ + "runBlocking {\n", + " readAction {\n", + " runBlockingCancellable {\n", + " repeat(5) {\n", + " launch(Dispatchers.Default) {\n", + " DISPLAY(\"Thread: ${Thread.currentThread()}; Read access: ${application.isReadAccessAllowed()}\")\n", + " }\n", + " }\n", + " }\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Thread: Thread[#1740,DefaultDispatcher-worker-10,5,main]; Read access: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Thread: Thread[#1555,DefaultDispatcher-worker-20,6,main]; Read access: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Thread: Thread[#1738,DefaultDispatcher-worker-18,5,main]; Read access: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Thread: Thread[#1292,DefaultDispatcher-worker-11,6,main]; Read access: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Thread: Thread[#1860,DefaultDispatcher-worker-26,5,main]; Read access: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Thread: Thread[#992,DefaultDispatcher-worker-4,5,main]; Read access: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Thread: Thread[#1610,DefaultDispatcher-worker-3,6,main]; Read access: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Thread: Thread[#1738,DefaultDispatcher-worker-18,5,main]; Read access: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Thread: Thread[#1299,DefaultDispatcher-worker-12,5,main]; Read access: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Thread: Thread[#1555,DefaultDispatcher-worker-20,6,main]; Read access: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Thread: Thread[#1345,DefaultDispatcher-worker-7,5,main]; Read access: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 36 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Locks and Modality State" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Due to the fact that locks are historically tightly coupled to EDT, they are affected by modality states." + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Modal Execution" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "When a modal computation emerges, its starts running a nested event loop. Modal computations are always initiated under _write-intent_ lock.\n", + "The code inside a modal computation is executed on background, but it can occasionally go to EDT and execute write actions.\n", + "\n", + "The lock is not held in any way inside modal computations. One has to acquire it explicitly." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T10:46:05.825836Z", + "start_time": "2025-08-05T10:46:05.596152Z" + } + }, + "cell_type": "code", + "source": [ + "runBlocking(Dispatchers.EDT) {\n", + " DISPLAY(\"Write-Intent lock outside modal progress: ${application.isWriteIntentLockAcquired}\")\n", + " runWithModalProgressBlocking(ModalTaskOwner.guess(), \"Sample\") {\n", + " DISPLAY(\"Current thread in modal progress: ${Thread.currentThread()}\")\n", + " DISPLAY(\"Write-Intent lock in modal progress: ${application.isWriteIntentLockAcquired}\")\n", + " DISPLAY(\"Read access in modal progress: ${application.isReadAccessAllowed}\")\n", + " runReadAction {\n", + " DISPLAY(\"Read access inside explicit read action: ${application.isReadAccessAllowed}\")\n", + " }\n", + " withContext(Dispatchers.EDT) {\n", + " DISPLAY(\"Write-Intent lock on EDT: ${application.isWriteIntentLockAcquired}\")\n", + " }\n", + " }\n", + "}\n" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Current thread in modal progress: Thread[#1299,DefaultDispatcher-worker-12,5,main]" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Write-Intent lock in modal progress: false" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Read access in modal progress: false" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Read access inside explicit read action: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Write-Intent lock on EDT: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Write-Intent lock outside modal progress: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 38 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Write-Safety" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "One of the goals of modality states is to prevent unrelated UI events from execution. The clients of IntelliJ Platform can bypass this restriction by executing their code with `ModalityState.any()`.\n", + "This has an interesting effect on write locks -- the use-case of `ModalityState.any()` is to run pure UI code, so the Platform needs to forbid the execution of write actions with `any`. Otherwise, anyone could sneak their model change inside any modal dialog that expects the consistent state of the world.\n", + "\n", + "The Platform extends its handling of `any` and write locks to the concept of _write-safety_, meaning that write actions are allowed only in write-safe contexts. The user's input is considered write-safe, and all modal computations initiated from write-safe contexts are also write-safe. All other contexts are write-unsafe." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-06T09:05:55.156314Z", + "start_time": "2025-08-06T09:05:54.782233Z" + } + }, + "cell_type": "code", + "source": [ + "import com.intellij.openapi.application.TransactionGuard\n", + "import javax.swing.SwingUtilities\n", + "\n", + "runBlocking {\n", + " withContext(Dispatchers.EDT) {\n", + " DISPLAY(\"Write-safety #1: ${TransactionGuard.getInstance().isWritingAllowed}\")\n", + " }\n", + " SwingUtilities.invokeAndWait {\n", + " DISPLAY(\"Write-safety #2: ${TransactionGuard.getInstance().isWritingAllowed}\")\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Write-safety #1: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "Write-safety #2: false" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 2 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Advanced: Background Write Actions" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "There is an ongoing effort on moving some write actions to background threads.\n", + "This functionality is **unstable** at the moment, so we give no promises of deadlock-freedom if you are using it in your code.\n", + "\n", + "Nevertheless, the intended way to use background write actions is via the corresponding suspending function:" + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-08-05T10:27:31.239357Z", + "start_time": "2025-08-05T10:27:31.124092Z" + } + }, + "cell_type": "code", + "source": [ + "runBlocking {\n", + " backgroundWriteAction {\n", + " DISPLAY(\"Write access: ${application.isWriteAccessAllowed}\")\n", + " DISPLAY(\"EDT: ${application.isDispatchThread}\")\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "Write access: true" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + }, + { + "data": { + "text/plain": [ + "EDT: false" + ] + }, + "metadata": {}, + "output_type": "display_data", + "jetTransient": { + "display_id": null + } + } + ], + "execution_count": 30 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Kotlin", + "language": "kotlin", + "name": "kotlin" + }, + "language_info": { + "name": "kotlin", + "version": "2.3.0-dev-704", + "mimetype": "text/x-kotlin", + "file_extension": ".kt", + "pygments_lexer": "kotlin", + "codemirror_mode": "text/x-kotlin", + "nbconvert_exporter": "" + }, + "ktnbPluginMetadata": { + "sessionRunMode": "IDE_PROCESS" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +}