Skip to content

Commit f75c534

Browse files
committed
Kotlin/Native multithreading (WIP)
* Kotlin version 1.3.70, using HMPP. * Provides newSingleThreadedContext. * Provides Dispatchers.Main on iOS, Dispatchers.Default everywhere. * Coroutine references (Job) and all kinds of channels are shareable across workers. * Each individual coroutine is confined to a single worker. Known problems: * There are some memory leaks.
1 parent 52dcddc commit f75c534

File tree

109 files changed

+3066
-895
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

109 files changed

+3066
-895
lines changed

README.md

+17-8
Original file line numberDiff line numberDiff line change
@@ -182,16 +182,23 @@ You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotli
182182
[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.3.5/jar)
183183
(follow the link to get the dependency declaration snippet).
184184

185-
Only single-threaded code (JS-style) on Kotlin/Native is currently supported.
186-
Kotlin/Native supports only Gradle version 4.10 and you need to enable Gradle metadata in your
187-
`settings.gradle` file:
185+
Kotlin/Native uses Gradle metadata and needs Gradle version 5.3 or later
186+
See [Gradle Metadata 1.0 announcement](https://blog.gradle.org/gradle-metadata-1.0) for more details.
188187

189-
```groovy
190-
enableFeaturePreview('GRADLE_METADATA')
191-
```
188+
Kotlin/Native does not generally provide binary compatibility between versions.
189+
You should use the same version of Kotlin/Native compiler as was used to build `kotlinx.coroutines`.
190+
191+
Kotlin/Native does not support free sharing of mutable objects between threads as on JVM, so several
192+
limitations apply to using coroutines on Kotlin/Native.
193+
See [Sharing and background threads on Kotlin/Native](kotlin-native-sharing.md) for details.
194+
195+
Some functions like [newSingleThreadContext] and [runBlocking] are available only for Kotlin/JVM and Kotlin/Native
196+
and are not available on Kotlin/JS. In order to access them from the code that is shared between JVM and Native
197+
you need to enable granular metadata (aka HMPP) in your `gradle.properties` file:
192198

193-
Since Kotlin/Native does not generally provide binary compatibility between versions,
194-
you should use the same version of Kotlin/Native compiler as was used to build `kotlinx.coroutines`.
199+
```properties
200+
kotlin.mpp.enableGranularSourceSetsMetadata=true
201+
```
195202

196203
## Building
197204

@@ -246,6 +253,8 @@ The `develop` branch is pushed to `master` during release.
246253
[Promise.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/kotlin.js.-promise/await.html
247254
[promise]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/promise.html
248255
[Window.asCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/org.w3c.dom.-window/as-coroutine-dispatcher.html
256+
[newSingleThreadContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-single-thread-context.html
257+
[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
249258
<!--- INDEX kotlinx.coroutines.flow -->
250259
[Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
251260
[_flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html

gradle.properties

+8-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#
44

55
# Kotlin
6-
version=1.3.5-SNAPSHOT
6+
version=1.3.5-native-mt-SNAPSHOT
77
group=org.jetbrains.kotlinx
88
kotlin_version=1.3.70
99

@@ -33,10 +33,12 @@ kotlin.js.compiler=both
3333
gradle_node_version=1.2.0
3434
node_version=8.9.3
3535
npm_version=5.7.1
36-
mocha_version=4.1.0
36+
mocha_version=6.2.2
3737
mocha_headless_chrome_version=1.8.2
38-
mocha_teamcity_reporter_version=2.2.2
39-
source_map_support_version=0.5.3
38+
mocha_teamcity_reporter_version=3.0.0
39+
source_map_support_version=0.5.16
40+
jsdom_version=15.2.1
41+
jsdom_global_version=3.0.2
4042

4143
# Settings
4244
kotlin.incremental.multiplatform=true
@@ -48,3 +50,5 @@ jekyll_version=4.0
4850
# JS IR baceknd sometimes crashes with out-of-memory
4951
# TODO: Remove once KT-37187 is fixed
5052
org.gradle.jvmargs=-Xmx2g
53+
54+
kotlin.mpp.enableGranularSourceSetsMetadata=true

gradle/compile-native-multiplatform.gradle

+14-26
Original file line numberDiff line numberDiff line change
@@ -13,36 +13,24 @@ kotlin {
1313
}
1414

1515
targets {
16-
if (project.ext.ideaActive) {
17-
fromPreset(project.ext.ideaPreset, 'native')
18-
} else {
19-
addTarget(presets.linuxX64)
20-
addTarget(presets.iosArm64)
21-
addTarget(presets.iosArm32)
22-
addTarget(presets.iosX64)
23-
addTarget(presets.macosX64)
24-
addTarget(presets.mingwX64)
25-
addTarget(presets.tvosArm64)
26-
addTarget(presets.tvosX64)
27-
addTarget(presets.watchosArm32)
28-
addTarget(presets.watchosArm64)
29-
addTarget(presets.watchosX86)
30-
}
16+
addTarget(presets.linuxX64)
17+
addTarget(presets.iosArm64)
18+
addTarget(presets.iosArm32)
19+
addTarget(presets.iosX64)
20+
addTarget(presets.macosX64)
21+
addTarget(presets.mingwX64)
22+
addTarget(presets.tvosArm64)
23+
addTarget(presets.tvosX64)
24+
addTarget(presets.watchosArm32)
25+
addTarget(presets.watchosArm64)
26+
addTarget(presets.watchosX86)
3127
}
3228

3329
sourceSets {
3430
nativeMain { dependsOn commonMain }
35-
// Empty source set is required in order to have native tests task
36-
nativeTest {}
31+
nativeTest { dependsOn commonTest }
3732

38-
if (!project.ext.ideaActive) {
39-
configure(nativeMainSets) {
40-
dependsOn nativeMain
41-
}
42-
43-
configure(nativeTestSets) {
44-
dependsOn nativeTest
45-
}
46-
}
33+
configure(nativeMainSets) { dependsOn nativeMain }
34+
configure(nativeTestSets) { dependsOn nativeTest }
4735
}
4836
}

gradle/targets.gradle

-28
This file was deleted.

gradle/test-mocha-js.gradle

+2-2
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ task testMochaChrome(type: NodeTask, dependsOn: prepareMochaChrome) {
7878
task installDependenciesMochaJsdom(type: NpmTask, dependsOn: [npmInstall]) {
7979
args = ['install',
8080
"mocha@$mocha_version",
81-
'jsdom@15.2.1',
82-
'jsdom-global@3.0.2',
81+
"jsdom@$jsdom_version",
82+
"jsdom-global@$jsdom_global_version",
8383
"source-map-support@$source_map_support_version",
8484
'--no-save']
8585
if (project.hasProperty("teamcity")) args += ["mocha-teamcity-reporter@$mocha_teamcity_reporter_version"]

kotlin-native-sharing.md

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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 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+
57+
## Switching threads
58+
59+
You switch from one dispatcher to another using a regular [withContext] function. For example, a code running
60+
on the main thread might do:
61+
62+
```kotlin
63+
// in the main thead
64+
val result = withContext(Dispatcher.Default) {
65+
// now executing in background thread
66+
}
67+
// now back to the main thread
68+
result // use result here
69+
```
70+
71+
If you capture a reference to any object that is defined in the main thread outside of `withContext` into the
72+
block inside `withContext` then it gets automatically frozen for transfer from the main thread to the
73+
background thread. Freezing is recursive, so you might accidentally freeze unrelated objects that are part of
74+
main thread's mutable state and get
75+
[InvalidMutabilityException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-invalid-mutability-exception/index.html)
76+
later in unrelated parts of your code.
77+
The easiest way to trouble-shoot it is to mark the objects that should not have been frozen using
78+
[ensureNeverFrozen](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/ensure-never-frozen.html)
79+
function so that you get exception in the very place they were frozen that would pinpoint the corresponding
80+
`withContext` call in your code.
81+
82+
The `result` of `withContext` call can be used after `withContext` call. It gets automatically frozen
83+
for transfer from background to the main thread, too.
84+
85+
A disciplined use of threads in Kotlin/Native is to transfer only immutable data between the threads.
86+
Such code works equally well both on Kotlin/JVM and Kotlin/Native.
87+
88+
> Note: freezing only happens when `withContext` changes from one thread to another. If you call
89+
> `withContext` and execution stays in the same thread, then there is not freezing and mutable data
90+
> can be captured and operated on as usual.
91+
92+
The same rule on freezing applies to coroutines launched with any builder like [launch], [async], [produce], etc.
93+
94+
## Communication objects
95+
96+
All core communication and synchronization objects in `kotlin.coroutines` such as
97+
[Job], [Deferred], [Channel], [BroadcastChannel], [Mutex], and [Semaphore] are _shareable_.
98+
It means that they can be frozen for sharing with another thread and still continue to operate normally.
99+
Any object that is transferred via a frozen (shared) [Deferred] or any [Channel] is also automatically frozen.
100+
101+
Similar rules apply to [Flow]. When an instance of a [Flow] itself is shared (frozen), then all the references that
102+
are captured in to the lambdas in this flow operators are frozen. Regardless of whether the flow instance itself
103+
was frozen, by default, the whole flow operates in a single thread, so mutable data can freely travel down the
104+
flow from emitter to collector. However, when [flowOn] operator is used to change the thread, then
105+
objects crossing the thread boundary get frozen.
106+
107+
Note, that if you protect any piece of mutable data with a [Mutex] or a [Semaphore] then it does not
108+
automatically become shareable. In order to share mutable data you have to either
109+
wrap it into [DetachedObjectGraph](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-detached-object-graph/index.html)
110+
or use atomic classes ([AtomicInt](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-atomic-int/index.html), etc).
111+
112+
## Cyclic garbage
113+
114+
Code working in a single thread on Kotlin/Native enjoys fully automatic memory management. Any object graph that
115+
is not referenced anymore is automatically reclaimed even if it contains cyclic chains of references. This does
116+
not extend to shared objects, though. Frozen immutable objects can be freely shared, even if then can contain
117+
reference cycles, but shareable [communication objects](#communication-objects) leak if a reference cycle
118+
to them appears. The easiest way to demonstrate it is to return a reference to a [async] coroutine as its result,
119+
so that the resulting [Deferred] contains a reference to itself:
120+
121+
```kotlin
122+
// from the main thread call coroutine in a background thread or otherwise share it
123+
val result = GlobalScope.async {
124+
coroutineContext // return its coroutine context that contains a self-reference
125+
}
126+
// now result will not be reclaimed -- memory leak
127+
```
128+
129+
A disciplined use of communication objects to transfer immutable data between coroutines does not
130+
result in any memory reclamation problems.
131+
132+
## Shared channels are resources
133+
134+
All kinds of [Channel] and [BroadcastChannel] implementations become _resources_ on Kotlin/Native when shared.
135+
They must be closed and fully consumed in order for their memory to be reclaimed. When they are not shared, they
136+
can be dropped in any state and will be reclaimed by memory manager, but a shared channel generally will not be reclaimed
137+
unless closed and consumed.
138+
139+
This does not affect [Flow], because it is a cold abstraction. Even though [Flow] internally uses channels to transfer
140+
data between threads, it always properly closes these channels when completing collection of data.
141+
142+
## Known problems
143+
144+
The current implementation is tested and works for all kinds of single-threaded cases and simple scenarios that
145+
transfer data between two thread like shown in [Switching Threads](#switching-threads) section. However, it is known
146+
to leak memory in scenarios involving concurrency under load, for example when multiple children coroutines running
147+
in different threads are simultaneously cancelled.
148+
149+
<!--- MODULE kotlinx-coroutines-core -->
150+
<!--- INDEX kotlinx.coroutines -->
151+
[newSingleThreadContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-single-thread-context.html
152+
[SingleThreadDispatcher.close]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-single-thread-dispatcher/-single-thread-dispatcher/close.html
153+
[SingleThreadDispatcher.worker]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-single-thread-dispatcher/-single-thread-dispatcher/worker.html
154+
[Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html
155+
[GlobalScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html
156+
[Dispatchers.Main]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html
157+
[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html
158+
[withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
159+
[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
160+
[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html
161+
[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html
162+
[Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html
163+
<!--- INDEX kotlinx.coroutines.flow -->
164+
[Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html
165+
[flowOn]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow-on.html
166+
<!--- INDEX kotlinx.coroutines.channels -->
167+
[produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html
168+
[Channel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html
169+
[BroadcastChannel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-broadcast-channel/index.html
170+
<!--- INDEX kotlinx.coroutines.selects -->
171+
<!--- INDEX kotlinx.coroutines.sync -->
172+
[Mutex]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html
173+
[Semaphore]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-semaphore/index.html
174+
<!--- END -->
175+

0 commit comments

Comments
 (0)