From c01d24388c5677f0b281be506a6381f9b7313838 Mon Sep 17 00:00:00 2001 From: Mikhail Fedotov Date: Mon, 26 Feb 2024 08:46:00 +0700 Subject: [PATCH] [docs] Add more info to Coroutines block --- .github/FUNDING.yml | 1 + README.md | 7 +++- docs/index.md | 37 +++++++++++++++---- .../kstatemachine/CoroutinesStateMachine.kt | 2 +- .../kotlin/ru/nsk/kstatemachine/TestUtils.kt | 4 +- 5 files changed, 40 insertions(+), 11 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index b32e6f8..b6bb61a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ ko_fi: nskfedotov +custom: https://yoomoney.ru/to/4100118569686448 \ No newline at end of file diff --git a/README.md b/README.md index db464ba..171e77d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ [![Android Arsenal]( https://img.shields.io/badge/Android%20Arsenal-KStateMachine-green.svg?style=flat )]( https://android-arsenal.com/details/1/8276 ) [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) ![multiplatform support](https://img.shields.io/badge/multiplatform-jvm%20%7C%20android%20%7C%20ios-brightgreen) -[![Share on X](https://img.shields.io/badge/twitter-share-white?logo=x&style=flat)](https://twitter.com/intent/tweet?text=I%20like%20this%20library%20%0A%0Ahttps%3A%2F%2Fgithub.com%2Fnsk90%2Fkstatemachine%0A%0A%23kstatemachine) +[![Share on X](https://img.shields.io/badge/twitter-share-white?logo=x&style=flat)](https://twitter.com/intent/tweet?text=I%20like%20KStateMachine%20library%20%0A%0Ahttps%3A%2F%2Fgithub.com%2Fnsk90%2Fkstatemachine%0A%0A%23kstatemachine) +[![Share on Reddit](https://img.shields.io/badge/reddit-share-red?logo=reddit&style=flat)](https://www.reddit.com/submit?url=https%3A%2F%2Fgithub.com%2Fnsk90%2Fkstatemachine&title=I%20like%20KStateMachine%20library) + [Documentation](https://nsk90.github.io/kstatemachine) | [Quick start](#quick-start-sample) | [Samples](#samples) | [Install](#install) | [License](#license) | [Discussions](https://github.com/nsk90/kstatemachine/discussions) @@ -62,7 +64,10 @@ State management features: > [!NOTE] > The library is in a development phase. You are welcome to propose useful features, or contribute to the project. +> > Don't forget to push the ⭐ if you like this project. +> +> You can donate or become a sponsor to support the project, using ❤️ github-sponsors button. ## Quick start sample diff --git a/docs/index.md b/docs/index.md index ce827a2..c224209 100644 --- a/docs/index.md +++ b/docs/index.md @@ -44,6 +44,8 @@ * [Other exceptions](#other-exceptions) * [Multithreading and concurrency](#multithreading-and-concurrency) * [Kotlin Coroutines](#kotlin-coroutines) + * [Use single threaded CoroutineScope](#use-single-threaded-coroutinescope) + * [CoroutineContext preservation guarantee](#coroutinecontext-preservation-guarantee) * [Additional kstatemachine-coroutines artifact](#additional-kstatemachine-coroutines-artifact) * [Migration guide from versions older than v0.20.0](#migration-guide-from-versions-older-than-v0200) * [Export](#export) @@ -825,7 +827,7 @@ Calling `processEvent()` on destroyed machine will throw also. KStateMachine is designed to work in single thread. Concurrent modification of library classes will lead to race conditions. -See [kotlin coroutines](#kotlin-coroutines) section for more info regarding coroutines environment, and how +See [kotlin coroutines](#kotlin-coroutines) section for more info regarding coroutines environment, and how the library helps you to support this requirement. ## Kotlin Coroutines @@ -842,13 +844,32 @@ Note that `Blocking` versions internally use `kotlinx.coroutines.runBlocking` fu may cause deadlocks if used not properly. That is why you should avoid using `Blocking` APIs from coroutines and recursively (from library callbacks). +### Use single threaded `CoroutineScope` + When you create a state machine with `createStateMachine`/`createStateMachineBlocking` (with coroutines support) -functions you have to provide `CoroutineScope` on which machine will work, +functions you have to provide `CoroutineScope` on which machine will work, this scope also contains `CoroutineContext` by coroutines design. This is how you can control a thread where state machine works. The scope is considered to use single threaded `CoroutineContext`. -Using multithreaded `CoroutineContext` like (`default` or `io`) will probably lead to race conditions, -this is not correct. + +Single thread `CoroutineScope` samples: + +```kotlin +CoroutineScope(newSingleThreadContext("single thread")) +CoroutineScope(Dispatchers.Main) +``` + +Using multithreaded `CoroutineContext` like `Dispatchers.Default` or `Dispatchers.IO` will lead to race +conditions, it is not correct. + +Even `Dispatchers.Default.limitedParallelism(1)` that seems to be ok at glance, +does not provide guarantee that each coroutine will be executed on the same single thread, it only limits the amount of +used threads. So race condition still takes place, as nothing forces threads, running on different processor cores, +to update variable values in their processor core caches, so outdated values could be used from core cache. Other words, +one thread does not to know about variable changes made by other one. This known as __visibility guarantee__, +that `volatile` keyword provides on `jvm`. + +### `CoroutineContext` preservation guarantee Suspendable functions and their `Blocking` analogs internally switch current execution `СoroutineСontext` (from which they are called) to state machines one, using `kotlinx.coroutines.withContext` or @@ -857,14 +878,16 @@ This is `CoroutineContext` preservation guarantee that the library provides. Note that if you created machine with the scope containing `kotlinx.coroutines.EmptyCoroutineContext` switching will not be performed. So if the StateMachine is created with correct (meeting above conditions) scope it is safe to call suspendable methods like `processEvent()` from any context/thread due to internal context preservation. -StateMachine that was created by `createStdLibStateMachine()` (without coroutines support) does not provide any context -switching and of course does NOT provide any `CotoutineContext` preservation guarantee. +StateMachine that was created by `createStdLibStateMachine()` (without coroutines support) does not perform any context +switching and of course does NOT provide any `CoroutineContext` preservation guarantee. Multithreading is always complicated and hard to explain, so you can also check this sample regarding working with state machine from coroutines running from multiple threads: ```kotlin -runBlocking { // defines non empty coroutine context for state machine +// runBlocking starts an infinite event loop on current running thread, +// so it produces correct single threaded CoroutineContext for a StateMachine. +runBlocking { // defines non-empty coroutine context for state machine val machineThread = Thread.currentThread() val machineScope = this diff --git a/kstatemachine-coroutines/src/commonMain/kotlin/ru/nsk/kstatemachine/CoroutinesStateMachine.kt b/kstatemachine-coroutines/src/commonMain/kotlin/ru/nsk/kstatemachine/CoroutinesStateMachine.kt index 6eca153..74273b7 100644 --- a/kstatemachine-coroutines/src/commonMain/kotlin/ru/nsk/kstatemachine/CoroutinesStateMachine.kt +++ b/kstatemachine-coroutines/src/commonMain/kotlin/ru/nsk/kstatemachine/CoroutinesStateMachine.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.CoroutineScope * @param scope be careful while working with threaded scopes as KStateMachine classes are not thread-safe. * Usually you should use only single threaded scopes, for example: * - * CoroutineScope(Dispatchers.Default.limitedParallelism(1)) + * CoroutineScope(newSingleThreadContext("single threaded context")) * * Note that all calls to created machine instance should be done only from that thread. */ diff --git a/tests/src/commonTest/kotlin/ru/nsk/kstatemachine/TestUtils.kt b/tests/src/commonTest/kotlin/ru/nsk/kstatemachine/TestUtils.kt index 9e8cf1b..741a45a 100644 --- a/tests/src/commonTest/kotlin/ru/nsk/kstatemachine/TestUtils.kt +++ b/tests/src/commonTest/kotlin/ru/nsk/kstatemachine/TestUtils.kt @@ -113,7 +113,7 @@ fun createTestStateMachine( init = init ) CoroutineStarterType.COROUTINES_LIB_SINGLE_THREAD_DISPATCHER -> createStateMachineBlocking( - CoroutineScope(newSingleThreadContext("")), + CoroutineScope(newSingleThreadContext("test single thread context")), name, childMode, start, @@ -123,7 +123,7 @@ fun createTestStateMachine( init = init ) CoroutineStarterType.COROUTINES_LIB_DEFAULT_LIMITED_DISPATCHER -> createStateMachineBlocking( - CoroutineScope(Dispatchers.Default.limitedParallelism(1)), + CoroutineScope(Dispatchers.Default.limitedParallelism(1)), // does not guarantee same thread for each task name, childMode, start,