|
| 1 | +# Sharing and background threads on Kotlin/Native |
| 2 | + |
| 3 | +## Preview disclaimer |
| 4 | + |
| 5 | +This is a preview release of sharing and backgrounds threads for coroutines on Kotlin/Native. |
| 6 | +Details of this implementation will change in the future. See also [Known Problems](#known-problems) |
| 7 | +at the end of this document. |
| 8 | + |
| 9 | +## Introduction |
| 10 | + |
| 11 | +Kotlin/Native provides an automated memory management that works with mutable data objects separately |
| 12 | +and independently in each thread that uses Kotlin/Native runtime. Sharing data between threads is limited: |
| 13 | + |
| 14 | +* Objects to be shared between threads can be [frozen](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/freeze.html). |
| 15 | + This makes the whole object graph deeply immutable and allows to share it between threads. |
| 16 | +* Mutable objects can be wrapped into [DetachedObjectGraph](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-detached-object-graph/index.html) |
| 17 | + on one thread and later reattached onto the different thread. |
| 18 | + |
| 19 | +This introduces several differences between Kotlin/JVM and Kotlin/Native in terms of coroutines that must |
| 20 | +be accounted for when writing cross-platform applications. |
| 21 | + |
| 22 | +## Threads and dispatchers |
| 23 | + |
| 24 | +An active coroutine has a mutable state. It cannot migrate from thread to thread. A coroutine in Kotlin/Native |
| 25 | +is always bound to a specific thread. Coroutines that are detached from a thread are currently not supported. |
| 26 | + |
| 27 | +`kotlinx.coroutines` provides ability to create single-threaded dispatchers for background work |
| 28 | +via [newSingleThreadContext] function that is available for both Kotlin/JVM and Kotlin/Native. It is not |
| 29 | +recommended shutting down such a dispatcher on Kotlin/Native via [SingleThreadDispatcher.close] function |
| 30 | +while the application still working unless you are absolutely sure all coroutines running in this |
| 31 | +dispatcher have completed. Unlike Kotlin/JVM, there is no backup default thread that might |
| 32 | +execute cleanup code for coroutines that might have been still working in this dispatcher. |
| 33 | + |
| 34 | +For interoperability with code that is using Kotlin/Native |
| 35 | +[Worker](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-worker/index.html) |
| 36 | +API you can get a reference to single-threaded dispacher's worker using its [SingleThreadDispatcher.worker] property. |
| 37 | + |
| 38 | +A [Default][Dispatchers.Default] dispatcher on Kotlin/Native contains a single background thread. |
| 39 | +This is the dispatcher that is used by default in [GlobalScope]. |
| 40 | + |
| 41 | +> This limitation may be lifted in the future with the default dispatcher becoming multi-threaded and/or |
| 42 | +> its coroutines becoming isolated from each other, so please do not assume that different coroutines running |
| 43 | +> in the default dispatcher can share mutable data between themselves. |
| 44 | +
|
| 45 | +A [Main][Dispatchers.Main] dispatcher is |
| 46 | +properly defined for all Darwin (Apple) targets, refers to the main thread, and integrates |
| 47 | +with Core Foundation main event loop. |
| 48 | +On Linux and Windows there is no platform-defined main thread, so [Main][Dispatchers.Main] simply refers |
| 49 | +to the current thread that must have been either created with `newSingleThreadContext` or be running |
| 50 | +inside [runBlocking] function. |
| 51 | + |
| 52 | +The main thread of application has two options on using coroutines. |
| 53 | +A backend application's main thread shall use [runBlocking]. |
| 54 | +A UI application running on one Apple's Darwin OSes shall run |
| 55 | +its main queue event loop using `NSRunLoopRun`, `UIApplicationMain`, or ` NSApplicationMain`. |
| 56 | +For example, that is how you can have main dispatcher in your own `main` function: |
| 57 | + |
| 58 | +```kotlin |
| 59 | +fun main() { |
| 60 | + val mainScope = MainScope() |
| 61 | + mainScope.launch { /* coroutine in the main thread */ } |
| 62 | + CFRunLoopRun() // run event loop |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +## Switching threads |
| 67 | + |
| 68 | +You switch from one dispatcher to another using a regular [withContext] function. For example, a code running |
| 69 | +on the main thread might do: |
| 70 | + |
| 71 | +```kotlin |
| 72 | +// in the main thead |
| 73 | +val result = withContext(Dispatcher.Default) { |
| 74 | + // now executing in background thread |
| 75 | +} |
| 76 | +// now back to the main thread |
| 77 | +result // use result here |
| 78 | +``` |
| 79 | + |
| 80 | +If you capture a reference to any object that is defined in the main thread outside of `withContext` into the |
| 81 | +block inside `withContext` then it gets automatically frozen for transfer from the main thread to the |
| 82 | +background thread. Freezing is recursive, so you might accidentally freeze unrelated objects that are part of |
| 83 | +main thread's mutable state and get |
| 84 | +[InvalidMutabilityException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-invalid-mutability-exception/index.html) |
| 85 | +later in unrelated parts of your code. |
| 86 | +The easiest way to trouble-shoot it is to mark the objects that should not have been frozen using |
| 87 | +[ensureNeverFrozen](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/ensure-never-frozen.html) |
| 88 | +function so that you get exception in the very place they were frozen that would pinpoint the corresponding |
| 89 | +`withContext` call in your code. |
| 90 | + |
| 91 | +The `result` of `withContext` call can be used after `withContext` call. It gets automatically frozen |
| 92 | +for transfer from background to the main thread, too. |
| 93 | + |
| 94 | +A disciplined use of threads in Kotlin/Native is to transfer only immutable data between the threads. |
| 95 | +Such code works equally well both on Kotlin/JVM and Kotlin/Native. |
| 96 | + |
| 97 | +> Note: freezing only happens when `withContext` changes from one thread to another. If you call |
| 98 | +> `withContext` and execution stays in the same thread, then there is not freezing and mutable data |
| 99 | +> can be captured and operated on as usual. |
| 100 | +
|
| 101 | +The same rule on freezing applies to coroutines launched with any builder like [launch], [async], [produce], etc. |
| 102 | + |
| 103 | +## Communication objects |
| 104 | + |
| 105 | +All core communication and synchronization objects in `kotlin.coroutines` such as |
| 106 | +[Job], [Deferred], [Channel], [BroadcastChannel], [Mutex], and [Semaphore] are _shareable_. |
| 107 | +It means that they can be frozen for sharing with another thread and still continue to operate normally. |
| 108 | +Any object that is transferred via a frozen (shared) [Deferred] or any [Channel] is also automatically frozen. |
| 109 | + |
| 110 | +Similar rules apply to [Flow]. When an instance of a [Flow] itself is shared (frozen), then all the references that |
| 111 | +are captured in to the lambdas in this flow operators are frozen. Regardless of whether the flow instance itself |
| 112 | +was frozen, by default, the whole flow operates in a single thread, so mutable data can freely travel down the |
| 113 | +flow from emitter to collector. However, when [flowOn] operator is used to change the thread, then |
| 114 | +objects crossing the thread boundary get frozen. |
| 115 | + |
| 116 | +Note, that if you protect any piece of mutable data with a [Mutex] or a [Semaphore] then it does not |
| 117 | +automatically become shareable. In order to share mutable data you have to either |
| 118 | +wrap it into [DetachedObjectGraph](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-detached-object-graph/index.html) |
| 119 | +or use atomic classes ([AtomicInt](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-atomic-int/index.html), etc). |
| 120 | + |
| 121 | +## Cyclic garbage |
| 122 | + |
| 123 | +Code working in a single thread on Kotlin/Native enjoys fully automatic memory management. Any object graph that |
| 124 | +is not referenced anymore is automatically reclaimed even if it contains cyclic chains of references. This does |
| 125 | +not extend to shared objects, though. Frozen immutable objects can be freely shared, even if then can contain |
| 126 | +reference cycles, but shareable [communication objects](#communication-objects) leak if a reference cycle |
| 127 | +to them appears. The easiest way to demonstrate it is to return a reference to a [async] coroutine as its result, |
| 128 | +so that the resulting [Deferred] contains a reference to itself: |
| 129 | + |
| 130 | +```kotlin |
| 131 | +// from the main thread call coroutine in a background thread or otherwise share it |
| 132 | +val result = GlobalScope.async { |
| 133 | + coroutineContext // return its coroutine context that contains a self-reference |
| 134 | +} |
| 135 | +// now result will not be reclaimed -- memory leak |
| 136 | +``` |
| 137 | + |
| 138 | +A disciplined use of communication objects to transfer immutable data between coroutines does not |
| 139 | +result in any memory reclamation problems. |
| 140 | + |
| 141 | +## Shared channels are resources |
| 142 | + |
| 143 | +All kinds of [Channel] and [BroadcastChannel] implementations become _resources_ on Kotlin/Native when shared. |
| 144 | +They must be closed and fully consumed in order for their memory to be reclaimed. When they are not shared, they |
| 145 | +can be dropped in any state and will be reclaimed by memory manager, but a shared channel generally will not be reclaimed |
| 146 | +unless closed and consumed. |
| 147 | + |
| 148 | +This does not affect [Flow], because it is a cold abstraction. Even though [Flow] internally uses channels to transfer |
| 149 | +data between threads, it always properly closes these channels when completing collection of data. |
| 150 | + |
| 151 | +## Known problems |
| 152 | + |
| 153 | +The current implementation is tested and works for all kinds of single-threaded cases and simple scenarios that |
| 154 | +transfer data between two thread like shown in [Switching Threads](#switching-threads) section. However, it is known |
| 155 | +to leak memory in scenarios involving concurrency under load, for example when multiple children coroutines running |
| 156 | +in different threads are simultaneously cancelled. |
| 157 | + |
| 158 | +<!--- MODULE kotlinx-coroutines-core --> |
| 159 | +<!--- INDEX kotlinx.coroutines --> |
| 160 | +[newSingleThreadContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-single-thread-context.html |
| 161 | +[SingleThreadDispatcher.close]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-single-thread-dispatcher/-single-thread-dispatcher/close.html |
| 162 | +[SingleThreadDispatcher.worker]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-single-thread-dispatcher/-single-thread-dispatcher/worker.html |
| 163 | +[Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html |
| 164 | +[GlobalScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html |
| 165 | +[Dispatchers.Main]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html |
| 166 | +[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html |
| 167 | +[withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html |
| 168 | +[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html |
| 169 | +[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html |
| 170 | +[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html |
| 171 | +[Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html |
| 172 | +<!--- INDEX kotlinx.coroutines.flow --> |
| 173 | +[Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html |
| 174 | +[flowOn]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow-on.html |
| 175 | +<!--- INDEX kotlinx.coroutines.channels --> |
| 176 | +[produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html |
| 177 | +[Channel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html |
| 178 | +[BroadcastChannel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-broadcast-channel/index.html |
| 179 | +<!--- INDEX kotlinx.coroutines.selects --> |
| 180 | +<!--- INDEX kotlinx.coroutines.sync --> |
| 181 | +[Mutex]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html |
| 182 | +[Semaphore]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-semaphore/index.html |
| 183 | +<!--- END --> |
| 184 | + |
0 commit comments