From c90afac776993cf89d864e0796824ea462173715 Mon Sep 17 00:00:00 2001 From: Roman Elizarov Date: Mon, 1 Jun 2020 14:46:41 +0300 Subject: [PATCH] Kotlin/Native multithreading * 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. * Update Dispatchers docs to account for native-mt changes. * Multithreaded support in select expression. Additional fixes: * Fixed broadcast builder with different thread * Fixed adding a child to a frozen parent job Fixes #462 Fixes #470 Fixes #765 Fixes #1645 Fixes #1751 Fixes #1828 Fixes #1831 Fixes #1764 ~ Kotlin 1.4-M2-eap-83 --- README.md | 27 +- gradle.properties | 2 +- kotlin-native-sharing.md | 184 ++++++++++ .../api/kotlinx-coroutines-core.api | 13 +- .../common/src/AbstractCoroutine.kt | 19 +- .../common/src/Builders.common.kt | 54 ++- .../common/src/CancellableContinuation.kt | 4 +- .../common/src/CancellableContinuationImpl.kt | 25 +- .../common/src/CoroutineContext.common.kt | 2 - .../common/src/CoroutineDispatcher.kt | 2 +- .../common/src/CoroutineStart.kt | 40 --- .../common/src/Dispatchers.common.kt | 19 +- .../common/src/EventLoop.common.kt | 28 +- .../common/src/Exceptions.common.kt | 2 +- .../common/src/JobSupport.kt | 69 ++-- kotlinx-coroutines-core/common/src/Timeout.kt | 9 +- .../common/src/channels/AbstractChannel.kt | 91 +++-- .../src/channels/ArrayBroadcastChannel.kt | 30 +- .../src/channels/ArrayBufferState.common.kt | 14 + .../common/src/channels/ArrayChannel.kt | 121 +++---- .../src/channels/ArrayChannelState.common.kt | 12 + .../common/src/channels/Broadcast.kt | 7 +- .../common/src/channels/ConflatedChannel.kt | 53 ++- .../channels/ConflatedChannelState.common.kt | 10 + .../common/src/flow/StateFlow.kt | 1 + .../common/src/internal/Concurrent.common.kt | 7 - .../common/src/internal/DispatchedTask.kt | 21 +- .../src/internal/LockFreeLinkedList.common.kt | 2 +- .../internal/ManualMemoryManagement.common.kt | 8 + .../common/src/internal/Scopes.kt | 9 +- .../common/src/internal/Sharing.common.kt | 30 ++ .../src/internal/Synchronized.common.kt | 19 -- .../common/src/internal/ThreadSafeHeap.kt | 1 + .../common/src/intrinsics/Undispatched.kt | 2 + .../common/src/selects/Select.kt | 44 ++- .../common/src/sync/Mutex.kt | 2 +- .../test/flow/operators/BackgroundFlowTest.kt | 24 ++ .../concurrent/src/Builders.concurrent.kt | 14 + .../concurrent/src/SingleThread.common.kt | 23 ++ .../src/internal/LockFreeLinkedList.kt | 108 ++++-- .../concurrent/test/StressUtil.common.kt | 7 + .../ConcurrentExceptionsStressTest.kt | 66 ++++ .../test/internal/LockFreeLinkedListTest.kt | 3 +- kotlinx-coroutines-core/js/src/Builders.kt | 34 ++ .../js/src/CoroutineContext.kt | 2 +- kotlinx-coroutines-core/js/src/Dispatchers.kt | 21 +- kotlinx-coroutines-core/js/src/Exceptions.kt | 3 +- .../js/src/channels/ArrayBufferState.kt | 20 ++ .../js/src/channels/ArrayChannelState.kt | 23 ++ .../js/src/channels/ConflatedChannelState.kt | 11 + .../js/src/internal/Concurrent.kt | 9 - .../js/src/internal/LinkedList.kt | 2 +- .../js/src/internal/ManualMemoryManagement.kt | 11 + .../js/src/internal/Sharing.kt | 73 ++++ .../js/src/internal/Synchronized.kt | 20 -- kotlinx-coroutines-core/jvm/src/Builders.kt | 29 +- .../jvm/src/CoroutineContext.kt | 3 +- kotlinx-coroutines-core/jvm/src/Exceptions.kt | 5 +- kotlinx-coroutines-core/jvm/src/Executors.kt | 2 +- .../jvm/src/ThreadPoolDispatcher.kt | 16 +- .../jvm/src/channels/ArrayBufferState.kt | 23 ++ .../jvm/src/channels/ArrayChannelState.kt | 23 ++ .../jvm/src/channels/ConflatedChannelState.kt | 14 + .../jvm/src/internal/Concurrent.kt | 10 +- .../src/internal/ManualMemoryManagement.kt | 17 + .../jvm/src/internal/Sharing.kt | 94 ++++++ .../jvm/src/internal/Synchronized.kt | 20 -- .../jvm/test/StressUtil.kt | 20 ++ .../jvm/test/exceptions/Exceptions.kt | 2 +- .../native/src/Builders.kt | 100 ++++-- .../native/src/CoroutineContext.kt | 7 +- .../native/src/Dispatchers.native.kt | 46 +++ .../native/src/EventLoop.kt | 56 +++- .../native/src/Exceptions.kt | 31 +- .../native/src/Thread.native.kt | 48 +++ kotlinx-coroutines-core/native/src/Workers.kt | 73 ++++ .../native/src/channels/ArrayBufferState.kt | 28 ++ .../native/src/channels/ArrayChannelState.kt | 33 ++ .../src/channels/ConflatedChannelState.kt | 21 ++ .../native/src/internal/Concurrent.kt | 9 - .../native/src/internal/CopyOnWriteList.kt | 85 ++--- .../native/src/internal/LinkedList.kt | 168 ---------- .../src/internal/ManualMemoryManagement.kt | 21 ++ .../native/src/internal/Sharing.kt | 222 ++++++++++++ .../native/src/internal/Synchronized.kt | 20 -- .../native/test/DefaultDispatcherTest.kt | 22 ++ .../native/test/EventLoopTest.kt | 55 +++ .../native/test/FreezingTest.kt | 53 +++ .../native/test/ParkStressTest.kt | 47 +++ .../native/test/StressUtil.kt | 23 ++ .../native/test/TestBase.kt | 51 +-- .../native/test/WorkerDispatcherTest.kt | 317 ++++++++++++++++++ .../native/test/WorkerTest.kt | 10 +- .../native/test/internal/LinkedListTest.kt | 47 --- .../nativeDarwin/src/Dispatchers.kt | 102 ++++++ .../nativeDarwin/src/Thread.kt | 69 ++++ .../nativeDarwin/test/Launcher.kt | 2 + .../nativeDarwin/test/MainDispatcherTest.kt | 122 +++++++ .../src/Dispatchers.kt | 17 +- .../nativeOther/src/Thread.kt | 11 + .../nativeOther/test/Launcher.kt | 1 + .../test/CoroutinesDumpTest.kt | 73 ++-- .../test/DebugProbesTest.kt | 19 +- .../test/StracktraceUtils.kt | 9 +- 104 files changed, 2876 insertions(+), 877 deletions(-) create mode 100644 kotlin-native-sharing.md create mode 100644 kotlinx-coroutines-core/common/src/channels/ArrayBufferState.common.kt create mode 100644 kotlinx-coroutines-core/common/src/channels/ArrayChannelState.common.kt create mode 100644 kotlinx-coroutines-core/common/src/channels/ConflatedChannelState.common.kt create mode 100644 kotlinx-coroutines-core/common/src/internal/ManualMemoryManagement.common.kt create mode 100644 kotlinx-coroutines-core/common/src/internal/Sharing.common.kt delete mode 100644 kotlinx-coroutines-core/common/src/internal/Synchronized.common.kt create mode 100644 kotlinx-coroutines-core/common/test/flow/operators/BackgroundFlowTest.kt create mode 100644 kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt create mode 100644 kotlinx-coroutines-core/concurrent/src/SingleThread.common.kt rename kotlinx-coroutines-core/{jvm => concurrent}/src/internal/LockFreeLinkedList.kt (86%) create mode 100644 kotlinx-coroutines-core/concurrent/test/StressUtil.common.kt create mode 100644 kotlinx-coroutines-core/concurrent/test/exceptions/ConcurrentExceptionsStressTest.kt rename kotlinx-coroutines-core/{jvm => concurrent}/test/internal/LockFreeLinkedListTest.kt (97%) create mode 100644 kotlinx-coroutines-core/js/src/Builders.kt create mode 100644 kotlinx-coroutines-core/js/src/channels/ArrayBufferState.kt create mode 100644 kotlinx-coroutines-core/js/src/channels/ArrayChannelState.kt create mode 100644 kotlinx-coroutines-core/js/src/channels/ConflatedChannelState.kt create mode 100644 kotlinx-coroutines-core/js/src/internal/ManualMemoryManagement.kt create mode 100644 kotlinx-coroutines-core/js/src/internal/Sharing.kt delete mode 100644 kotlinx-coroutines-core/js/src/internal/Synchronized.kt create mode 100644 kotlinx-coroutines-core/jvm/src/channels/ArrayBufferState.kt create mode 100644 kotlinx-coroutines-core/jvm/src/channels/ArrayChannelState.kt create mode 100644 kotlinx-coroutines-core/jvm/src/channels/ConflatedChannelState.kt create mode 100644 kotlinx-coroutines-core/jvm/src/internal/ManualMemoryManagement.kt create mode 100644 kotlinx-coroutines-core/jvm/src/internal/Sharing.kt delete mode 100644 kotlinx-coroutines-core/jvm/src/internal/Synchronized.kt create mode 100644 kotlinx-coroutines-core/jvm/test/StressUtil.kt create mode 100644 kotlinx-coroutines-core/native/src/Dispatchers.native.kt create mode 100644 kotlinx-coroutines-core/native/src/Thread.native.kt create mode 100644 kotlinx-coroutines-core/native/src/Workers.kt create mode 100644 kotlinx-coroutines-core/native/src/channels/ArrayBufferState.kt create mode 100644 kotlinx-coroutines-core/native/src/channels/ArrayChannelState.kt create mode 100644 kotlinx-coroutines-core/native/src/channels/ConflatedChannelState.kt delete mode 100644 kotlinx-coroutines-core/native/src/internal/LinkedList.kt create mode 100644 kotlinx-coroutines-core/native/src/internal/ManualMemoryManagement.kt create mode 100644 kotlinx-coroutines-core/native/src/internal/Sharing.kt delete mode 100644 kotlinx-coroutines-core/native/src/internal/Synchronized.kt create mode 100644 kotlinx-coroutines-core/native/test/DefaultDispatcherTest.kt create mode 100644 kotlinx-coroutines-core/native/test/EventLoopTest.kt create mode 100644 kotlinx-coroutines-core/native/test/FreezingTest.kt create mode 100644 kotlinx-coroutines-core/native/test/ParkStressTest.kt create mode 100644 kotlinx-coroutines-core/native/test/StressUtil.kt create mode 100644 kotlinx-coroutines-core/native/test/WorkerDispatcherTest.kt delete mode 100644 kotlinx-coroutines-core/native/test/internal/LinkedListTest.kt create mode 100644 kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt create mode 100644 kotlinx-coroutines-core/nativeDarwin/src/Thread.kt create mode 100644 kotlinx-coroutines-core/nativeDarwin/test/MainDispatcherTest.kt rename kotlinx-coroutines-core/{native => nativeOther}/src/Dispatchers.kt (68%) create mode 100644 kotlinx-coroutines-core/nativeOther/src/Thread.kt diff --git a/README.md b/README.md index 795616c88d..fa8bd10493 100644 --- a/README.md +++ b/README.md @@ -180,19 +180,24 @@ You can also use [`kotlinx-coroutines-core`](https://www.npmjs.com/package/kotli ### Native [Kotlin/Native](https://kotlinlang.org/docs/reference/native-overview.html) version of `kotlinx.coroutines` is published as -[`kotlinx-coroutines-core-native`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-native/1.3.7/jar) -(follow the link to get the dependency declaration snippet). +[`kotlinx-coroutines-core`](https://search.maven.org/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core/1.3.7/jar) +(follow the link to get the dependency declaration snippet). **Kotlin/Native requires Gradle version 6.0 or later** +to resolve that dependency properly into the corresponding platform-specific artifacts. -Only single-threaded code (JS-style) on Kotlin/Native is currently supported. -Kotlin/Native supports only Gradle version 4.10 and you need to enable Gradle metadata in your -`settings.gradle` file: +Kotlin/Native does not generally provide binary compatibility between versions. +You should use the same version of Kotlin/Native compiler as was used to build `kotlinx.coroutines`. -```groovy -enableFeaturePreview('GRADLE_METADATA') -``` +Kotlin/Native does not support free sharing of mutable objects between threads as on JVM, so several +limitations apply to using coroutines on Kotlin/Native. +See [Sharing and background threads on Kotlin/Native](kotlin-native-sharing.md) for details. + +Some functions like [newSingleThreadContext] and [runBlocking] are available only for Kotlin/JVM and Kotlin/Native +and are not available on Kotlin/JS. In order to access them from the code that is shared between JVM and Native +you need to enable granular metadata (aka HMPP) in your `gradle.properties` file: -Since Kotlin/Native does not generally provide binary compatibility between versions, -you should use the same version of Kotlin/Native compiler as was used to build `kotlinx.coroutines`. +```properties +kotlin.mpp.enableGranularSourceSetsMetadata=true +``` ## Building @@ -247,6 +252,8 @@ The `develop` branch is pushed to `master` during release. [Promise.await]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/kotlin.js.-promise/await.html [promise]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/promise.html [Window.asCoroutineDispatcher]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/org.w3c.dom.-window/as-coroutine-dispatcher.html +[newSingleThreadContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-single-thread-context.html +[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html [Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html [_flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow.html diff --git a/gradle.properties b/gradle.properties index 1d139f9ec8..8a5be60f1c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ # # Kotlin -version=1.3.7-1.4-M2-eap-83-1 +version=1.3.7-native-mt-1.4-M2-eap-83-SNAPSHOT group=org.jetbrains.kotlinx kotlin_version=1.4-M2-eap-83 diff --git a/kotlin-native-sharing.md b/kotlin-native-sharing.md new file mode 100644 index 0000000000..46c7069ccf --- /dev/null +++ b/kotlin-native-sharing.md @@ -0,0 +1,184 @@ +# Sharing and background threads on Kotlin/Native + +## Preview disclaimer + +This is a preview release of sharing and backgrounds threads for coroutines on Kotlin/Native. +Details of this implementation will change in the future. See also [Known Problems](#known-problems) +at the end of this document. + +## Introduction + +Kotlin/Native provides an automated memory management that works with mutable data objects separately +and independently in each thread that uses Kotlin/Native runtime. Sharing data between threads is limited: + +* Objects to be shared between threads can be [frozen](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/freeze.html). + This makes the whole object graph deeply immutable and allows to share it between threads. +* Mutable objects can be wrapped into [DetachedObjectGraph](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-detached-object-graph/index.html) + on one thread and later reattached onto the different thread. + +This introduces several differences between Kotlin/JVM and Kotlin/Native in terms of coroutines that must +be accounted for when writing cross-platform applications. + +## Threads and dispatchers + +An active coroutine has a mutable state. It cannot migrate from thread to thread. A coroutine in Kotlin/Native +is always bound to a specific thread. Coroutines that are detached from a thread are currently not supported. + +`kotlinx.coroutines` provides ability to create single-threaded dispatchers for background work +via [newSingleThreadContext] function that is available for both Kotlin/JVM and Kotlin/Native. It is not +recommended shutting down such a dispatcher on Kotlin/Native via [SingleThreadDispatcher.close] function +while the application still working unless you are absolutely sure all coroutines running in this +dispatcher have completed. Unlike Kotlin/JVM, there is no backup default thread that might +execute cleanup code for coroutines that might have been still working in this dispatcher. + +For interoperability with code that is using Kotlin/Native +[Worker](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-worker/index.html) +API you can get a reference to single-threaded dispacher's worker using its [SingleThreadDispatcher.worker] property. + +A [Default][Dispatchers.Default] dispatcher on Kotlin/Native contains a single background thread. +This is the dispatcher that is used by default in [GlobalScope]. + +> This limitation may be lifted in the future with the default dispatcher becoming multi-threaded and/or +> its coroutines becoming isolated from each other, so please do not assume that different coroutines running +> in the default dispatcher can share mutable data between themselves. + +A [Main][Dispatchers.Main] dispatcher is +properly defined for all Darwin (Apple) targets, refers to the main thread, and integrates +with Core Foundation main event loop. +On Linux and Windows there is no platform-defined main thread, so [Main][Dispatchers.Main] simply refers +to the current thread that must have been either created with `newSingleThreadContext` or be running +inside [runBlocking] function. + +The main thread of application has two options on using coroutines. +A backend application's main thread shall use [runBlocking]. +A UI application running on one Apple's Darwin OSes shall run +its main queue event loop using `NSRunLoopRun`, `UIApplicationMain`, or ` NSApplicationMain`. +For example, that is how you can have main dispatcher in your own `main` function: + +```kotlin +fun main() { + val mainScope = MainScope() + mainScope.launch { /* coroutine in the main thread */ } + CFRunLoopRun() // run event loop +} +``` + +## Switching threads + +You switch from one dispatcher to another using a regular [withContext] function. For example, a code running +on the main thread might do: + +```kotlin +// in the main thead +val result = withContext(Dispatcher.Default) { + // now executing in background thread +} +// now back to the main thread +result // use result here +``` + +If you capture a reference to any object that is defined in the main thread outside of `withContext` into the +block inside `withContext` then it gets automatically frozen for transfer from the main thread to the +background thread. Freezing is recursive, so you might accidentally freeze unrelated objects that are part of +main thread's mutable state and get +[InvalidMutabilityException](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-invalid-mutability-exception/index.html) +later in unrelated parts of your code. +The easiest way to trouble-shoot it is to mark the objects that should not have been frozen using +[ensureNeverFrozen](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/ensure-never-frozen.html) +function so that you get exception in the very place they were frozen that would pinpoint the corresponding +`withContext` call in your code. + +The `result` of `withContext` call can be used after `withContext` call. It gets automatically frozen +for transfer from background to the main thread, too. + +A disciplined use of threads in Kotlin/Native is to transfer only immutable data between the threads. +Such code works equally well both on Kotlin/JVM and Kotlin/Native. + +> Note: freezing only happens when `withContext` changes from one thread to another. If you call +> `withContext` and execution stays in the same thread, then there is not freezing and mutable data +> can be captured and operated on as usual. + +The same rule on freezing applies to coroutines launched with any builder like [launch], [async], [produce], etc. + +## Communication objects + +All core communication and synchronization objects in `kotlin.coroutines` such as +[Job], [Deferred], [Channel], [BroadcastChannel], [Mutex], and [Semaphore] are _shareable_. +It means that they can be frozen for sharing with another thread and still continue to operate normally. +Any object that is transferred via a frozen (shared) [Deferred] or any [Channel] is also automatically frozen. + +Similar rules apply to [Flow]. When an instance of a [Flow] itself is shared (frozen), then all the references that +are captured in to the lambdas in this flow operators are frozen. Regardless of whether the flow instance itself +was frozen, by default, the whole flow operates in a single thread, so mutable data can freely travel down the +flow from emitter to collector. However, when [flowOn] operator is used to change the thread, then +objects crossing the thread boundary get frozen. + +Note, that if you protect any piece of mutable data with a [Mutex] or a [Semaphore] then it does not +automatically become shareable. In order to share mutable data you have to either +wrap it into [DetachedObjectGraph](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-detached-object-graph/index.html) +or use atomic classes ([AtomicInt](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.concurrent/-atomic-int/index.html), etc). + +## Cyclic garbage + +Code working in a single thread on Kotlin/Native enjoys fully automatic memory management. Any object graph that +is not referenced anymore is automatically reclaimed even if it contains cyclic chains of references. This does +not extend to shared objects, though. Frozen immutable objects can be freely shared, even if then can contain +reference cycles, but shareable [communication objects](#communication-objects) leak if a reference cycle +to them appears. The easiest way to demonstrate it is to return a reference to a [async] coroutine as its result, +so that the resulting [Deferred] contains a reference to itself: + +```kotlin +// from the main thread call coroutine in a background thread or otherwise share it +val result = GlobalScope.async { + coroutineContext // return its coroutine context that contains a self-reference +} +// now result will not be reclaimed -- memory leak +``` + +A disciplined use of communication objects to transfer immutable data between coroutines does not +result in any memory reclamation problems. + +## Shared channels are resources + +All kinds of [Channel] and [BroadcastChannel] implementations become _resources_ on Kotlin/Native when shared. +They must be closed and fully consumed in order for their memory to be reclaimed. When they are not shared, they +can be dropped in any state and will be reclaimed by memory manager, but a shared channel generally will not be reclaimed +unless closed and consumed. + +This does not affect [Flow], because it is a cold abstraction. Even though [Flow] internally uses channels to transfer +data between threads, it always properly closes these channels when completing collection of data. + +## Known problems + +The current implementation is tested and works for all kinds of single-threaded cases and simple scenarios that +transfer data between two thread like shown in [Switching Threads](#switching-threads) section. However, it is known +to leak memory in scenarios involving concurrency under load, for example when multiple children coroutines running +in different threads are simultaneously cancelled. + + + +[newSingleThreadContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-single-thread-context.html +[SingleThreadDispatcher.close]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-single-thread-dispatcher/-single-thread-dispatcher/close.html +[SingleThreadDispatcher.worker]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-single-thread-dispatcher/-single-thread-dispatcher/worker.html +[Dispatchers.Default]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html +[GlobalScope]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html +[Dispatchers.Main]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html +[runBlocking]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html +[withContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html +[launch]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html +[async]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html +[Job]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html +[Deferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html + +[Flow]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html +[flowOn]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/flow-on.html + +[produce]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/produce.html +[Channel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html +[BroadcastChannel]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-broadcast-channel/index.html + + +[Mutex]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-mutex/index.html +[Semaphore]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.sync/-semaphore/index.html + + diff --git a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api index 6a24b6a23a..971cabee86 100644 --- a/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api +++ b/kotlinx-coroutines-core/api/kotlinx-coroutines-core.api @@ -13,7 +13,6 @@ public abstract class kotlinx/coroutines/AbstractCoroutine : kotlinx/coroutines/ protected fun onStart ()V public final fun resumeWith (Ljava/lang/Object;)V public final fun start (Lkotlinx/coroutines/CoroutineStart;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)V - public final fun start (Lkotlinx/coroutines/CoroutineStart;Lkotlin/jvm/functions/Function1;)V } public final class kotlinx/coroutines/AwaitKt { @@ -61,6 +60,7 @@ public class kotlinx/coroutines/CancellableContinuationImpl : kotlin/coroutines/ public fun getCallerFrame ()Lkotlin/coroutines/jvm/internal/CoroutineStackFrame; public fun getContext ()Lkotlin/coroutines/CoroutineContext; public fun getContinuationCancellationCause (Lkotlinx/coroutines/Job;)Ljava/lang/Throwable; + public synthetic fun getDelegate$kotlinx_coroutines_core ()Lkotlin/coroutines/Continuation; public final fun getResult ()Ljava/lang/Object; public fun getStackTraceElement ()Ljava/lang/StackTraceElement; public fun initCancellability ()V @@ -80,10 +80,8 @@ public class kotlinx/coroutines/CancellableContinuationImpl : kotlin/coroutines/ public final class kotlinx/coroutines/CancellableContinuationKt { public static final fun disposeOnCancellation (Lkotlinx/coroutines/CancellableContinuation;Lkotlinx/coroutines/DisposableHandle;)V - public static final fun suspendAtomicCancellableCoroutine (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun suspendAtomicCancellableCoroutine (ZLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static synthetic fun suspendAtomicCancellableCoroutine$default (ZLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; - public static final fun suspendCancellableCoroutine (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public abstract interface class kotlinx/coroutines/ChildHandle : kotlinx/coroutines/DisposableHandle { @@ -156,7 +154,7 @@ public abstract class kotlinx/coroutines/CoroutineDispatcher : kotlin/coroutines public abstract fun dispatch (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V public fun dispatchYield (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Runnable;)V public fun get (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext$Element; - public final fun interceptContinuation (Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; + public fun interceptContinuation (Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; public fun isDispatchNeeded (Lkotlin/coroutines/CoroutineContext;)Z public fun minusKey (Lkotlin/coroutines/CoroutineContext$Key;)Lkotlin/coroutines/CoroutineContext; public final fun plus (Lkotlinx/coroutines/CoroutineDispatcher;)Lkotlinx/coroutines/CoroutineDispatcher; @@ -225,8 +223,6 @@ public final class kotlinx/coroutines/CoroutineStart : java/lang/Enum { public static final field DEFAULT Lkotlinx/coroutines/CoroutineStart; public static final field LAZY Lkotlinx/coroutines/CoroutineStart; public static final field UNDISPATCHED Lkotlinx/coroutines/CoroutineStart; - public final fun invoke (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)V - public final fun invoke (Lkotlin/jvm/functions/Function2;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)V public final fun isLazy ()Z public static fun valueOf (Ljava/lang/String;)Lkotlinx/coroutines/CoroutineStart; public static fun values ()[Lkotlinx/coroutines/CoroutineStart; @@ -503,6 +499,11 @@ public final class kotlinx/coroutines/SupervisorKt { public static final fun supervisorScope (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class kotlinx/coroutines/SuspendCancellableCoroutineKt { + public static final fun suspendAtomicCancellableCoroutine (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun suspendCancellableCoroutine (Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public abstract interface class kotlinx/coroutines/ThreadContextElement : kotlin/coroutines/CoroutineContext$Element { public abstract fun restoreThreadContext (Lkotlin/coroutines/CoroutineContext;Ljava/lang/Object;)V public abstract fun updateThreadContext (Lkotlin/coroutines/CoroutineContext;)Ljava/lang/Object; diff --git a/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt b/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt index 742c9670b7..3ef1f993be 100644 --- a/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt +++ b/kotlinx-coroutines-core/common/src/AbstractCoroutine.kt @@ -124,23 +124,6 @@ public abstract class AbstractCoroutine( return "\"$coroutineName\":${super.nameString()}" } - /** - * Starts this coroutine with the given code [block] and [start] strategy. - * This function shall be invoked at most once on this coroutine. - * - * First, this function initializes parent job from the `parentContext` of this coroutine that was passed to it - * during construction. Second, it starts the coroutine based on [start] parameter: - * - * * [DEFAULT] uses [startCoroutineCancellable]. - * * [ATOMIC] uses [startCoroutine]. - * * [UNDISPATCHED] uses [startCoroutineUndispatched]. - * * [LAZY] does nothing. - */ - public fun start(start: CoroutineStart, block: suspend () -> T) { - initParentJob() - start(block, this) - } - /** * Starts this coroutine with the given code [block] and [start] strategy. * This function shall be invoked at most once on this coroutine. @@ -155,6 +138,6 @@ public abstract class AbstractCoroutine( */ public fun start(start: CoroutineStart, receiver: R, block: suspend R.() -> T) { initParentJob() - start(block, receiver, this) + startCoroutine(start, receiver, this, block) } } diff --git a/kotlinx-coroutines-core/common/src/Builders.common.kt b/kotlinx-coroutines-core/common/src/Builders.common.kt index 7dd1b174ee..9ee2adc66f 100644 --- a/kotlinx-coroutines-core/common/src/Builders.common.kt +++ b/kotlinx-coroutines-core/common/src/Builders.common.kt @@ -106,10 +106,10 @@ private class LazyDeferredCoroutine( parentContext: CoroutineContext, block: suspend CoroutineScope.() -> T ) : DeferredCoroutine(parentContext, active = false) { - private val continuation = block.createCoroutineUnintercepted(this, this) + private val saved = saveLazyCoroutine(this, this, block) override fun onStart() { - continuation.startCoroutineCancellable(this) + startLazyCoroutine(saved, this, this) } } @@ -157,7 +157,7 @@ public suspend fun withContext( // SLOW PATH -- use new dispatcher val coroutine = DispatchedCoroutine(newContext, uCont) coroutine.initParentJob() - block.startCoroutineCancellable(coroutine, coroutine) + startCoroutine(CoroutineStart.DEFAULT, coroutine, coroutine, block) coroutine.getResult() } @@ -172,6 +172,45 @@ public suspend inline operator fun CoroutineDispatcher.invoke( noinline block: suspend CoroutineScope.() -> T ): T = withContext(this, block) +internal fun startCoroutineImpl( + start: CoroutineStart, + receiver: R, + completion: Continuation, + block: suspend R.() -> T +) = when (start) { + CoroutineStart.DEFAULT -> block.startCoroutineCancellable(receiver, completion) + CoroutineStart.ATOMIC -> block.startCoroutine(receiver, completion) + CoroutineStart.UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion) + CoroutineStart.LAZY -> Unit // will start lazily +} + +// --------------- Kotlin/Native specialization hooks --------------- + +// todo: impl a separate startCoroutineCancellable as a fast-path for startCoroutine(DEFAULT, ...) +internal expect fun startCoroutine( + start: CoroutineStart, + receiver: R, + completion: Continuation, + block: suspend R.() -> T +) + +/** + * On JVM & JS lazy coroutines are eagerly started (to record creation trace), the started later. + * On Native the block is saved so that it can be shared with another worker, the created and started later. + */ +internal expect fun saveLazyCoroutine( + coroutine: AbstractCoroutine, + receiver: R, + block: suspend R.() -> T +): Any + +// saved == result of saveLazyCoroutine that was stored in LazyXxxCoroutine class +internal expect fun startLazyCoroutine( + saved: Any, + coroutine: AbstractCoroutine, + receiver: R +) + // --------------- implementation --------------- private open class StandaloneCoroutine( @@ -188,10 +227,9 @@ private class LazyStandaloneCoroutine( parentContext: CoroutineContext, block: suspend CoroutineScope.() -> Unit ) : StandaloneCoroutine(parentContext, active = false) { - private val continuation = block.createCoroutineUnintercepted(this, this) - + private val saved = saveLazyCoroutine(this, this, block) override fun onStart() { - continuation.startCoroutineCancellable(this) + startLazyCoroutine(saved, this, this) } } @@ -214,7 +252,7 @@ private const val SUSPENDED = 1 private const val RESUMED = 2 // Used by withContext when context dispatcher changes -private class DispatchedCoroutine( +internal class DispatchedCoroutine( context: CoroutineContext, uCont: Continuation ) : ScopeCoroutine(context, uCont) { @@ -251,7 +289,7 @@ private class DispatchedCoroutine( override fun afterResume(state: Any?) { if (tryResume()) return // completed before getResult invocation -- bail out // Resume in a cancellable way because we have to switch back to the original dispatcher - uCont.intercepted().resumeCancellableWith(recoverResult(state, uCont)) + uCont.shareableInterceptedResumeCancellableWith(recoverResult(state, uCont)) } fun getResult(): Any? { diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt index 0d3fe847dc..ea582dfd0f 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuation.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuation.kt @@ -233,7 +233,9 @@ internal suspend inline fun suspendAtomicCancellableCoroutineReusable( internal fun getOrCreateCancellableContinuation(delegate: Continuation): CancellableContinuationImpl { // If used outside of our dispatcher - if (delegate !is DispatchedContinuation) { + // NOTE: Reuse is not support on Kotlin/Native due to platform peculiarities making it had to properly + // split DispatchedContinuation / CancellableContinuationImpl state across workers. + if (!isReuseSupportedInPlatform() || delegate !is DispatchedContinuation ) { return CancellableContinuationImpl(delegate, resumeMode = MODE_ATOMIC_DEFAULT) } /* diff --git a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt index e25ebd3a37..8c7e8bcc12 100644 --- a/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt +++ b/kotlinx-coroutines-core/common/src/CancellableContinuationImpl.kt @@ -24,9 +24,11 @@ internal val RESUME_TOKEN = Symbol("RESUME_TOKEN") */ @PublishedApi internal open class CancellableContinuationImpl( - final override val delegate: Continuation, + delegate: Continuation, resumeMode: Int ) : DispatchedTask(resumeMode), CancellableContinuation, CoroutineStackFrame { + @PublishedApi // for Kotlin/Native + final override val delegate: Continuation = delegate.asShareable() public override val context: CoroutineContext = delegate.context /* @@ -85,7 +87,9 @@ internal open class CancellableContinuationImpl( setupCancellation() } - private fun isReusable(): Boolean = delegate is DispatchedContinuation<*> && delegate.isReusable(this) + // todo: It is never reusable on Kotlin/Native due to architectural peculiarities + private fun isReusable(): Boolean = + delegate is DispatchedContinuation<*> && delegate.isReusable(this) && isReuseSupportedInPlatform() /** * Resets cancellability state in order to [suspendAtomicCancellableCoroutineReusable] to work. @@ -113,7 +117,7 @@ internal open class CancellableContinuationImpl( private fun setupCancellation() { if (checkCompleted()) return if (parentHandle !== null) return // fast path 2 -- was already initialized - val parent = delegate.context[Job] ?: return // fast path 3 -- don't do anything without parent + val parent = context[Job] ?: return // fast path 3 -- don't do anything without parent parent.start() // make sure the parent is started val handle = parent.invokeOnCompletion( onCancelling = true, @@ -131,6 +135,9 @@ internal open class CancellableContinuationImpl( private fun checkCompleted(): Boolean { val completed = isCompleted if (resumeMode != MODE_ATOMIC_DEFAULT) return completed // Do not check postponed cancellation for non-reusable continuations + // This check is needed in advance because we cannot check if delegate is DispatchedContinuation<*>. + // The stable reference could have been already disposed and we cannot even safely grab it concurrently with dispose + if (!isReuseSupportedInPlatform()) return completed val dispatched = delegate as? DispatchedContinuation<*> ?: return completed val cause = dispatched.checkPostponedCancellation(this) ?: return completed if (!completed) { @@ -141,7 +148,7 @@ internal open class CancellableContinuationImpl( } public override val callerFrame: CoroutineStackFrame? - get() = delegate as? CoroutineStackFrame + get() = delegate.asLocal() as? CoroutineStackFrame public override fun getStackTraceElement(): StackTraceElement? = null @@ -160,6 +167,7 @@ internal open class CancellableContinuationImpl( */ private fun cancelLater(cause: Throwable): Boolean { if (resumeMode != MODE_ATOMIC_DEFAULT) return false + // On Kotlin/Native reuse is not supported, so delegate is never DispatchedContinuation and false is returned val dispatched = (delegate as? DispatchedContinuation<*>) ?: return false return dispatched.postponeCancellation(cause) } @@ -179,7 +187,8 @@ internal open class CancellableContinuationImpl( } } - internal fun parentCancelled(cause: Throwable) { + internal fun parentCancelled(parentJob: Job) { + val cause = getContinuationCancellationCause(parentJob) if (cancelLater(cause)) return cancel(cause) // Even if cancellation has failed, we should detach child to avoid potential leak @@ -228,6 +237,8 @@ internal open class CancellableContinuationImpl( internal fun getResult(): Any? { setupCancellation() if (trySuspend()) return COROUTINE_SUSPENDED + // When cancellation does not suspend on Kotlin/Native it shall dispose its continuation which it will not use + disposeContinuation { delegate } // otherwise, onCompletionInternal was already invoked & invoked tryResume, and the result is in the state val state = this.state if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this) @@ -396,12 +407,12 @@ internal open class CancellableContinuationImpl( } override fun CoroutineDispatcher.resumeUndispatched(value: T) { - val dc = delegate as? DispatchedContinuation + val dc = delegate.asLocalOrNullIfNotUsed() as? DispatchedContinuation resumeImpl(value, if (dc?.dispatcher === this) MODE_UNDISPATCHED else resumeMode) } override fun CoroutineDispatcher.resumeUndispatchedWithException(exception: Throwable) { - val dc = delegate as? DispatchedContinuation + val dc = delegate.asLocalOrNullIfNotUsed() as? DispatchedContinuation resumeImpl(CompletedExceptionally(exception), if (dc?.dispatcher === this) MODE_UNDISPATCHED else resumeMode) } diff --git a/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt b/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt index 51374603c3..1e5f1a556c 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineContext.common.kt @@ -12,8 +12,6 @@ import kotlin.coroutines.* */ public expect fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext -internal expect fun createDefaultDispatcher(): CoroutineDispatcher - @Suppress("PropertyName") internal expect val DefaultDelay: Delay diff --git a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt index 1b6e7eb00f..289a9bedff 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineDispatcher.kt @@ -95,7 +95,7 @@ public abstract class CoroutineDispatcher : * This method should generally be exception-safe. An exception thrown from this method * may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state. */ - public final override fun interceptContinuation(continuation: Continuation): Continuation = + public override fun interceptContinuation(continuation: Continuation): Continuation = DispatchedContinuation(this, continuation) @InternalCoroutinesApi diff --git a/kotlinx-coroutines-core/common/src/CoroutineStart.kt b/kotlinx-coroutines-core/common/src/CoroutineStart.kt index 05e80e3ed7..d01394f9ff 100644 --- a/kotlinx-coroutines-core/common/src/CoroutineStart.kt +++ b/kotlinx-coroutines-core/common/src/CoroutineStart.kt @@ -5,8 +5,6 @@ package kotlinx.coroutines import kotlinx.coroutines.CoroutineStart.* -import kotlinx.coroutines.intrinsics.* -import kotlin.coroutines.* /** * Defines start options for coroutines builders. @@ -74,44 +72,6 @@ public enum class CoroutineStart { @ExperimentalCoroutinesApi UNDISPATCHED; - /** - * Starts the corresponding block as a coroutine with this coroutine's start strategy. - * - * * [DEFAULT] uses [startCoroutineCancellable]. - * * [ATOMIC] uses [startCoroutine]. - * * [UNDISPATCHED] uses [startCoroutineUndispatched]. - * * [LAZY] does nothing. - * - * @suppress **This an internal API and should not be used from general code.** - */ - @InternalCoroutinesApi - public operator fun invoke(block: suspend () -> T, completion: Continuation): Unit = - when (this) { - DEFAULT -> block.startCoroutineCancellable(completion) - ATOMIC -> block.startCoroutine(completion) - UNDISPATCHED -> block.startCoroutineUndispatched(completion) - LAZY -> Unit // will start lazily - } - - /** - * Starts the corresponding block with receiver as a coroutine with this coroutine start strategy. - * - * * [DEFAULT] uses [startCoroutineCancellable]. - * * [ATOMIC] uses [startCoroutine]. - * * [UNDISPATCHED] uses [startCoroutineUndispatched]. - * * [LAZY] does nothing. - * - * @suppress **This an internal API and should not be used from general code.** - */ - @InternalCoroutinesApi - public operator fun invoke(block: suspend R.() -> T, receiver: R, completion: Continuation): Unit = - when (this) { - DEFAULT -> block.startCoroutineCancellable(receiver, completion) - ATOMIC -> block.startCoroutine(receiver, completion) - UNDISPATCHED -> block.startCoroutineUndispatched(receiver, completion) - LAZY -> Unit // will start lazily - } - /** * Returns `true` when [LAZY]. * diff --git a/kotlinx-coroutines-core/common/src/Dispatchers.common.kt b/kotlinx-coroutines-core/common/src/Dispatchers.common.kt index dba57abc88..4d9db565d9 100644 --- a/kotlinx-coroutines-core/common/src/Dispatchers.common.kt +++ b/kotlinx-coroutines-core/common/src/Dispatchers.common.kt @@ -15,8 +15,11 @@ public expect object Dispatchers { * [launch][CoroutineScope.launch], [async][CoroutineScope.async], etc * if neither a dispatcher nor any other [ContinuationInterceptor] is specified in their context. * - * It is backed by a shared pool of threads on JVM. By default, the maximum number of threads used - * by this dispatcher is equal to the number of CPU cores, but is at least two. + * Its implementation depends on the platform: + * - On Kotlin/JVM it is backed by a shared pool of threads. By default, the maximum number of threads used + * by this dispatcher is equal to the number of CPU cores, but is at least two. + * - On Kotlin/JS it backed by the JS event loop. + * - On Kotlin/Native it is backed by a single background thread that is created on the first use. */ public val Default: CoroutineDispatcher @@ -26,15 +29,17 @@ public expect object Dispatchers { * * Access to this property may throw an [IllegalStateException] if no main dispatchers are present in the classpath. * - * Depending on platform and classpath it can be mapped to different dispatchers: - * - On JS and Native it is equivalent to the [Default] dispatcher. - * - On JVM it either the Android main thread dispatcher, JavaFx or Swing EDT dispatcher. It is chosen by the + * Depending on the platform and classpath it can be mapped to different dispatchers: + * - On Kotlin/JVM it either the Android main thread dispatcher, JavaFx or Swing EDT dispatcher. It is chosen by the * [`ServiceLoader`](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html). + * - On Kotlin/JS it is equivalent to the [Default] dispatcher. + * - On Kotlin/Native Apple platforms it maps to the Darwin main thread. + * - On other Kotlin/Native platforms it is equivalent to the [Default] dispatcher. * - * In order to work with the `Main` dispatcher, the following artifact should be added to the project runtime dependencies: + * In order to work with the `Main` dispatcher on Kotlin/JVM, the following artifact should be added to the project runtime dependencies: * - `kotlinx-coroutines-android` — for Android Main thread dispatcher * - `kotlinx-coroutines-javafx` — for JavaFx Application thread dispatcher - * - `kotlinx-coroutines-swing` — for Swing EDT dispatcher + * - `kotlinx-coroutines-swing` — for Swing EDT dispatcher. * * Implementation note: [MainCoroutineDispatcher.immediate] is not supported on the Native and JS platforms. */ diff --git a/kotlinx-coroutines-core/common/src/EventLoop.common.kt b/kotlinx-coroutines-core/common/src/EventLoop.common.kt index 69ea9fe312..3f50dda1f8 100644 --- a/kotlinx-coroutines-core/common/src/EventLoop.common.kt +++ b/kotlinx-coroutines-core/common/src/EventLoop.common.kt @@ -230,8 +230,8 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay { val timeNanos = delayToNanos(timeMillis) if (timeNanos < MAX_DELAY_NS) { val now = nanoTime() - DelayedResumeTask(now + timeNanos, continuation).also { task -> - continuation.disposeOnCancellation(task) + DelayedResumeTask(now + timeNanos, continuation, asShareable()).also { task -> + continuation.disposeOnCancellation(task.asShareable()) schedule(now, task) } } @@ -243,7 +243,7 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay { val now = nanoTime() DelayedRunnableTask(now + timeNanos, block).also { task -> schedule(now, task) - } + }.asShareable() } else { NonDisposableHandle } @@ -404,7 +404,7 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay { * into heap to avoid overflow and corruption of heap data structure. */ @JvmField var nanoTime: Long - ) : Runnable, Comparable, DisposableHandle, ThreadSafeHeapNode { + ) : ShareableRefHolder(), Runnable, Comparable, DisposableHandle, ThreadSafeHeapNode { private var _heap: Any? = null // null | ThreadSafeHeap | DISPOSED_TASK override var heap: ThreadSafeHeap<*>? @@ -477,25 +477,31 @@ internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay { @Suppress("UNCHECKED_CAST") (heap as? DelayedTaskQueue)?.remove(this) // remove if it is in heap (first) _heap = DISPOSED_TASK // never add again to any heap + disposeSharedRef() } - override fun toString(): String = "Delayed[nanos=$nanoTime]" + override fun toString(): String = "Delayed@$hexAddress[nanos=$nanoTime]" } - private inner class DelayedResumeTask( + private class DelayedResumeTask( nanoTime: Long, - private val cont: CancellableContinuation + private val cont: CancellableContinuation, + private val dispatcher: CoroutineDispatcher ) : DelayedTask(nanoTime) { - override fun run() { with(cont) { resumeUndispatched(Unit) } } - override fun toString(): String = super.toString() + cont.toString() + override fun run() { + disposeSharedRef() + with(cont) { dispatcher.resumeUndispatched(Unit) } + } } private class DelayedRunnableTask( nanoTime: Long, private val block: Runnable ) : DelayedTask(nanoTime) { - override fun run() { block.run() } - override fun toString(): String = super.toString() + block.toString() + override fun run() { + disposeSharedRef() + block.run() + } } /** diff --git a/kotlinx-coroutines-core/common/src/Exceptions.common.kt b/kotlinx-coroutines-core/common/src/Exceptions.common.kt index 64f8911e9d..973a1b6708 100644 --- a/kotlinx-coroutines-core/common/src/Exceptions.common.kt +++ b/kotlinx-coroutines-core/common/src/Exceptions.common.kt @@ -22,7 +22,7 @@ internal expect class JobCancellationException( cause: Throwable?, job: Job ) : CancellationException { - internal val job: Job + internal val job: Job? } internal class CoroutinesInternalError(message: String, cause: Throwable) : Error(message, cause) diff --git a/kotlinx-coroutines-core/common/src/JobSupport.kt b/kotlinx-coroutines-core/common/src/JobSupport.kt index 020d00a32c..3658e14d12 100644 --- a/kotlinx-coroutines-core/common/src/JobSupport.kt +++ b/kotlinx-coroutines-core/common/src/JobSupport.kt @@ -6,6 +6,7 @@ package kotlinx.coroutines import kotlinx.atomicfu.* +import kotlinx.atomicfu.locks.* import kotlinx.coroutines.internal.* import kotlinx.coroutines.intrinsics.* import kotlinx.coroutines.selects.* @@ -238,6 +239,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren assert { casSuccess } // And process all post-completion actions completeStateFinalization(state, finalState) + disposeLockFreeLinkedList { state.list } // only needed on Kotlin/Native return finalState } @@ -293,6 +295,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren onCancelling(null) // simple state is not a failure onCompletionInternal(update) completeStateFinalization(state, update) + disposeLockFreeLinkedList { state as? JobNode<*> } // only needed on Kotlin/Native return true } @@ -499,6 +502,7 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren } } else -> { // is complete + disposeLockFreeLinkedList { nodeCache } // :KLUDGE: We have to invoke a handler in platform-specific way via `invokeIt` extension, // because we play type tricks on Kotlin/JS and handler is not necessarily a function there if (invokeImmediately) handler.invokeIt((state as? CompletedExceptionally)?.cause) @@ -524,11 +528,15 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // try to promote it to LIST state with the corresponding state val list = NodeList() val update = if (state.isActive) list else InactiveNodeList(list) - _state.compareAndSet(state, update) + if (!_state.compareAndSet(state, update)) { + disposeLockFreeLinkedList { list } + } } private fun promoteSingleToNodeList(state: JobNode<*>) { // try to promote it to list (SINGLE+ state) + // Note: on Kotlin/Native we don't have to dispose NodeList() that we've failed to add, since + // it does not have cyclic references when addOneIfEmpty fails. state.addOneIfEmpty(NodeList()) // it must be in SINGLE+ state or state has changed (node could have need removed from state) val list = state.nextNode // either our NodeList or somebody else won the race, updated state @@ -589,7 +597,10 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren is JobNode<*> -> { // SINGE/SINGLE+ state -- one completion handler if (state !== node) return // a different job node --> we were already removed // try remove and revert back to empty state - if (_state.compareAndSet(state, EMPTY_ACTIVE)) return + if (_state.compareAndSet(state, EMPTY_ACTIVE)) { + disposeLockFreeLinkedList { state } + return + } } is Incomplete -> { // may have a list of completion handlers // remove node from the list if there is a list @@ -787,7 +798,11 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren val list = getOrPromoteCancellingList(state) ?: return false // Create cancelling state (with rootCause!) val cancelling = Finishing(list, false, rootCause) - if (!_state.compareAndSet(state, cancelling)) return false + if (!_state.compareAndSet(state, cancelling)) { + // Dispose if the list was just freshly allocated + disposeLockFreeLinkedList { list.takeIf { list !== state.list } } + return false + } // Notify listeners notifyCancelling(list, rootCause) return true @@ -883,7 +898,11 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // We do it as early is possible while still holding the lock. This ensures that we cancelImpl asap // (if somebody else is faster) and we synchronize all the threads on this finishing lock asap. if (finishing !== state) { - if (!_state.compareAndSet(state, finishing)) return COMPLETING_RETRY + if (!_state.compareAndSet(state, finishing)) { + // Dispose if the list was just freshly allocated + disposeLockFreeLinkedList { list.takeIf { list !== state.list } } + return COMPLETING_RETRY + } } // ## IMPORTANT INVARIANT: Only one thread (that had set isCompleting) can go past this point assert { !finishing.isSealed } // cannot be sealed @@ -1096,15 +1115,20 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren // Seals current state and returns list of exceptions // guarded by `synchronized(this)` fun sealLocked(proposedException: Throwable?): List { - val list = when(val eh = exceptionsHolder) { // volatile read + var list = when(val eh = exceptionsHolder) { // volatile read null -> allocateList() is Throwable -> allocateList().also { it.add(eh) } is ArrayList<*> -> eh as ArrayList else -> error("State is $eh") // already sealed -- cannot happen } val rootCause = this.rootCause // volatile read - rootCause?.let { list.add(0, it) } // note -- rootCause goes to the beginning - if (proposedException != null && proposedException != rootCause) list.add(proposedException) + rootCause?.let { + // note -- rootCause goes to the beginning + list.addOrUpdate(0, it) { list = it } + } + if (proposedException != null && proposedException != rootCause) { + list.addOrUpdate(proposedException) { list = it } + } exceptionsHolder = SEALED return list } @@ -1124,10 +1148,9 @@ public open class JobSupport constructor(active: Boolean) : Job, ChildJob, Paren exceptionsHolder = allocateList().apply { add(eh) add(exception) - } } - is ArrayList<*> -> (eh as ArrayList).add(exception) + is ArrayList<*> -> (eh as ArrayList).addOrUpdate(exception) { exceptionsHolder = it } else -> error("State is $eh") // already sealed -- cannot happen } } @@ -1310,8 +1333,6 @@ private class Empty(override val isActive: Boolean) : Incomplete { } internal open class JobImpl(parent: Job?) : JobSupport(true), CompletableJob { - init { initParentJobInternal(parent) } - override val onCancelComplete get() = true /* * Check whether parent is able to handle exceptions as well. * With this check, an exception in that pattern will be handled once: @@ -1322,18 +1343,24 @@ internal open class JobImpl(parent: Job?) : JobSupport(true), CompletableJob { * } * ``` */ - override val handlesException: Boolean = handlesException() + override val handlesException: Boolean = handlesException(parent) + + /* + * Only after that we init parent job (which might freeze this object if parent is frozen). + */ + init { initParentJobInternal(parent) } + + override val onCancelComplete get() = true + override fun complete() = makeCompleting(Unit) override fun completeExceptionally(exception: Throwable): Boolean = makeCompleting(CompletedExceptionally(exception)) @JsName("handlesExceptionF") - private fun handlesException(): Boolean { - var parentJob = (parentHandle as? ChildHandleNode)?.job ?: return false - while (true) { - if (parentJob.handlesException) return true - parentJob = (parentJob.parentHandle as? ChildHandleNode)?.job ?: return false - } + private tailrec fun handlesException(parent: Job?): Boolean { + val parentJob = (parent as? JobSupport) ?: return false + if (parentJob.handlesException) return true + return handlesException((parentJob.parentHandle as? ChildHandleNode)?.job) } } @@ -1480,9 +1507,9 @@ internal class ChildContinuation( parent: Job, @JvmField val child: CancellableContinuationImpl<*> ) : JobCancellingNode(parent) { - override fun invoke(cause: Throwable?) { - child.parentCancelled(child.getContinuationCancellationCause(job)) - } + override fun invoke(cause: Throwable?) = + child.parentCancelled(job) + override fun toString(): String = "ChildContinuation[$child]" } diff --git a/kotlinx-coroutines-core/common/src/Timeout.kt b/kotlinx-coroutines-core/common/src/Timeout.kt index 87fe733773..1ee77dc6bd 100644 --- a/kotlinx-coroutines-core/common/src/Timeout.kt +++ b/kotlinx-coroutines-core/common/src/Timeout.kt @@ -75,7 +75,7 @@ public suspend fun withTimeoutOrNull(timeMillis: Long, block: suspend Corout } } catch (e: TimeoutCancellationException) { // Return null if it's our exception, otherwise propagate it upstream (e.g. in case of nested withTimeouts) - if (e.coroutine === coroutine) { + if (e.coroutine.unweakRef() === coroutine) { return null } throw e @@ -103,8 +103,7 @@ private fun setupTimeout( block: suspend CoroutineScope.() -> T ): Any? { // schedule cancellation of this coroutine on time - val cont = coroutine.uCont - val context = cont.context + val context = coroutine.context coroutine.disposeOnCompletion(context.delay.invokeOnTimeout(coroutine.time, coroutine)) // restart the block using a new coroutine with a new job, // however, start it undispatched, because we already are in the proper context @@ -128,7 +127,7 @@ private class TimeoutCoroutine( */ public class TimeoutCancellationException internal constructor( message: String, - @JvmField internal val coroutine: Job? + @JvmField internal val coroutine: Any? ) : CancellationException(message), CopyableThrowable { /** * Creates a timeout exception with the given message. @@ -146,4 +145,4 @@ public class TimeoutCancellationException internal constructor( internal fun TimeoutCancellationException( time: Long, coroutine: Job -) : TimeoutCancellationException = TimeoutCancellationException("Timed out waiting for $time ms", coroutine) +) : TimeoutCancellationException = TimeoutCancellationException("Timed out waiting for $time ms", coroutine.weakRef()) diff --git a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt index 28c7ceabe1..0e6a501212 100644 --- a/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/AbstractChannel.kt @@ -78,7 +78,7 @@ internal abstract class AbstractSendChannel : SendChannel { * Returns non-null closed token if it is last in the queue. * @suppress **This is unstable API and it is subject to change.** */ - protected val closedForSend: Closed<*>? get() = (queue.prevNode as? Closed<*>)?.also { helpClose(it) } + protected val closedForSend: Closed<*>? get() = (queueTail() as? Closed<*>)?.also { helpClose(it) } /** * Returns non-null closed token if it is first in the queue. @@ -115,9 +115,9 @@ internal abstract class AbstractSendChannel : SendChannel { queue: LockFreeLinkedListHead, element: E ) : AddLastDesc>(queue, SendBuffered(element)) { - override fun failure(affected: LockFreeLinkedListNode): Any? = when (affected) { + override fun failure(affected: LockFreeLinkedListNode?): Any? = when (affected) { is Closed<*> -> affected - is ReceiveOrClosed<*> -> OFFER_FAILED + is ReceiveOrClosed<*>? -> OFFER_FAILED // must fail on null for unlinked nodes on K/N else -> null } } @@ -237,7 +237,8 @@ internal abstract class AbstractSendChannel : SendChannel { * "if (!close()) next send will throw" */ val closeAdded = queue.addLastIfPrev(closed) { it !is Closed<*> } - val actuallyClosed = if (closeAdded) closed else queue.prevNode as Closed<*> + val actuallyClosed = if (closeAdded) closed else queueTail() as Closed<*> + disposeLockFreeLinkedList { closed.takeUnless { closeAdded } } helpClose(actuallyClosed) if (closeAdded) invokeOnCloseHandler(cause) return closeAdded // true if we have closed @@ -312,6 +313,12 @@ internal abstract class AbstractSendChannel : SendChannel { closedList.forEachReversed { it.resumeReceiveClosed(closed) } // and do other post-processing onClosedIdempotent(closed) + // dispose on Kotlin/Native if closed is the only element in the queue now + disposeQueue { closed } + } + + internal inline fun disposeQueue(closed: () -> Closed<*>?) { + disposeLockFreeLinkedList { queue.takeIf { it.nextNode === closed() } } } /** @@ -341,9 +348,9 @@ internal abstract class AbstractSendChannel : SendChannel { @JvmField val element: E, queue: LockFreeLinkedListHead ) : RemoveFirstDesc>(queue) { - override fun failure(affected: LockFreeLinkedListNode): Any? = when (affected) { + override fun failure(affected: LockFreeLinkedListNode?): Any? = when (affected) { is Closed<*> -> affected - !is ReceiveOrClosed<*> -> OFFER_FAILED + !is ReceiveOrClosed<*> -> OFFER_FAILED // must fail on null for unlinked nodes on K/N else -> null } @@ -412,7 +419,7 @@ internal abstract class AbstractSendChannel : SendChannel { is Send -> "SendQueued" else -> "UNEXPECTED:$head" // should not happen } - val tail = queue.prevNode + val tail = queueTail() if (tail !== head) { result += ",queueSize=${countQueueSize()}" if (tail is Closed<*>) result += ",closedForSend=$tail" @@ -420,6 +427,13 @@ internal abstract class AbstractSendChannel : SendChannel { return result } + private fun queueTail(): LockFreeLinkedListNode { + // Backwards links can be already unlinked on Kotlin/Native when it was closed and only + // a closed node remains in it. queue.prevNode returns queue in this case + val tail = queue.prevNode + return if (tail === queue) queue.nextNode else tail + } + private fun countQueueSize(): Int { var size = 0 queue.forEach { size++ } @@ -434,13 +448,15 @@ internal abstract class AbstractSendChannel : SendChannel { override val pollResult: Any?, @JvmField val channel: AbstractSendChannel, @JvmField val select: SelectInstance, - @JvmField val block: suspend (SendChannel) -> R + block: suspend (SendChannel) -> R ) : Send(), DisposableHandle { + @JvmField val block: suspend (SendChannel) -> R = block.asShareable() + override fun tryResumeSend(otherOp: PrepareOp?): Symbol? = select.trySelectOther(otherOp) as Symbol? // must return symbol override fun completeResumeSend() { - block.startCoroutine(receiver = channel, completion = select.completion) + startCoroutine(CoroutineStart.ATOMIC, channel, select.completion, block) } override fun dispose() { // invoked on select completion @@ -495,6 +511,7 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel } val token = send.tryResumeSend(null) if (token != null) { assert { token === RESUME_TOKEN } @@ -620,6 +637,8 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel : AbstractSendChannel(), Channel() while (true) { val previous = closed.prevNode - if (previous is LockFreeLinkedListHead) { - break - } + // It could be already unlinked on Kotlin/Native and close.prevNode === closed + if (previous is LockFreeLinkedListHead || previous === closed) break assert { previous is Send } if (!previous.remove()) { previous.helpRemove() // make sure remove is complete before continuing @@ -662,9 +680,9 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel(queue: LockFreeLinkedListHead) : RemoveFirstDesc(queue) { - override fun failure(affected: LockFreeLinkedListNode): Any? = when (affected) { + override fun failure(affected: LockFreeLinkedListNode?): Any? = when (affected) { is Closed<*> -> affected - !is Send -> POLL_FAILED + !is Send -> POLL_FAILED // must fail on null for unlinked nodes on K/N else -> null } @@ -794,7 +812,10 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel(val channel: AbstractChannel) : ChannelIterator { - var result: Any? = POLL_FAILED // E | POLL_FAILED | Closed + private val _result = atomic(POLL_FAILED) // E | POLL_FAILED | Closed + var result: Any? + get() = _result.value + set(value) { _result.value = value } override suspend fun hasNext(): Boolean { // check for repeated hasNext @@ -852,9 +873,11 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel( - @JvmField val cont: CancellableContinuation, + cont: CancellableContinuation, @JvmField val receiveMode: Int ) : Receive() { + private val _cont = atomic?>(cont) + fun resumeValue(value: E): Any? = when (receiveMode) { RECEIVE_RESULT -> ValueOrClosed.value(value) else -> value @@ -862,31 +885,35 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) { + val cont = _cont.getAndSet(null)!! when { receiveMode == RECEIVE_NULL_ON_CLOSE && closed.closeCause == null -> cont.resume(null) receiveMode == RECEIVE_RESULT -> cont.resume(closed.toResult()) else -> cont.resumeWithException(closed.receiveException) } } + override fun toString(): String = "ReceiveElement@$hexAddress[receiveMode=$receiveMode]" } private class ReceiveHasNext( @JvmField val iterator: Itr, - @JvmField val cont: CancellableContinuation + cont: CancellableContinuation ) : Receive() { + private val _cont = atomic?>(cont) + override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Symbol? { - val token = cont.tryResume(true, otherOp?.desc) ?: return null + val token = _cont.value?.tryResume(true, otherOp?.desc) ?: return null assert { token === RESUME_TOKEN } // the only other possible result // We can call finishPrepare only after successful tryResume, so that only good affected node is saved otherOp?.finishPrepare() @@ -899,10 +926,11 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel) { + val cont = _cont.getAndSet(null)!! val token = if (closed.closeCause == null) { cont.tryResume(false) } else { @@ -913,30 +941,33 @@ internal abstract class AbstractChannel : AbstractSendChannel(), Channel( @JvmField val channel: AbstractChannel, @JvmField val select: SelectInstance, - @JvmField val block: suspend (Any?) -> R, + block: suspend (Any?) -> R, @JvmField val receiveMode: Int ) : Receive(), DisposableHandle { + @JvmField val block: suspend (Any?) -> R = block.asShareable() // captured variables in this block need screening + override fun tryResumeReceive(value: E, otherOp: PrepareOp?): Symbol? = select.trySelectOther(otherOp) as Symbol? @Suppress("UNCHECKED_CAST") override fun completeResumeReceive(value: E) { - block.startCoroutine(if (receiveMode == RECEIVE_RESULT) ValueOrClosed.value(value) else value, select.completion) + startCoroutine(CoroutineStart.ATOMIC, if (receiveMode == RECEIVE_RESULT) ValueOrClosed.value(value) else value, select.completion, block) } override fun resumeReceiveClosed(closed: Closed<*>) { if (!select.trySelect()) return when (receiveMode) { RECEIVE_THROWS_ON_CLOSE -> select.resumeSelectWithException(closed.receiveException) - RECEIVE_RESULT -> block.startCoroutine(ValueOrClosed.closed(closed.closeCause), select.completion) + RECEIVE_RESULT -> startCoroutine(CoroutineStart.ATOMIC, ValueOrClosed.closed(closed.closeCause), select.completion, block) RECEIVE_NULL_ON_CLOSE -> if (closed.closeCause == null) { - block.startCoroutine(null, select.completion) + startCoroutine(CoroutineStart.ATOMIC, null, select.completion, block) } else { select.resumeSelectWithException(closed.receiveException) } @@ -1012,17 +1043,19 @@ internal interface ReceiveOrClosed { @Suppress("UNCHECKED_CAST") internal class SendElement( override val pollResult: Any?, - @JvmField val cont: CancellableContinuation + cont: CancellableContinuation ) : Send() { + private val _cont = atomic?>(cont) + override fun tryResumeSend(otherOp: PrepareOp?): Symbol? { - val token = cont.tryResume(Unit, otherOp?.desc) ?: return null + val token = _cont.value?.tryResume(Unit, otherOp?.desc) ?: return null assert { token === RESUME_TOKEN } // the only other possible result // We can call finishPrepare only after successful tryResume, so that only good affected node is saved otherOp?.finishPrepare() // finish preparations return RESUME_TOKEN } - override fun completeResumeSend() = cont.completeResume(RESUME_TOKEN) - override fun resumeSendClosed(closed: Closed<*>) = cont.resumeWithException(closed.sendException) + override fun completeResumeSend() { _cont.getAndSet(null)!!.completeResume(RESUME_TOKEN) } + override fun resumeSendClosed(closed: Closed<*>) { _cont.getAndSet(null)!!.resumeWithException(closed.sendException) } override fun toString(): String = "SendElement@$hexAddress($pollResult)" } diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt index 155652fd6f..2b886103dc 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayBroadcastChannel.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines.channels import kotlinx.atomicfu.* +import kotlinx.atomicfu.locks.* import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import kotlinx.coroutines.selects.* @@ -39,8 +40,7 @@ internal class ArrayBroadcastChannel( * - Read "tail" (volatile), then read element from buffer * So read/writes to buffer need not be volatile */ - private val bufferLock = ReentrantLock() - private val buffer = arrayOfNulls(capacity) + private val state = ArrayBufferState(capacity) // head & tail are Long (64 bits) and we assume that they never wrap around // head, tail, and size are guarded by bufferLock @@ -91,13 +91,13 @@ internal class ArrayBroadcastChannel( // result is `OFFER_SUCCESS | OFFER_FAILED | Closed` override fun offerInternal(element: E): Any { - bufferLock.withLock { + state.withLock { // check if closed for send (under lock, so size cannot change) closedForSend?.let { return it } val size = this.size if (size >= capacity) return OFFER_FAILED val tail = this.tail - buffer[(tail % capacity).toInt()] = element + state.setBufferAt((tail % capacity).toInt(), element) this.size = size + 1 this.tail = tail + 1 } @@ -108,7 +108,7 @@ internal class ArrayBroadcastChannel( // result is `ALREADY_SELECTED | OFFER_SUCCESS | OFFER_FAILED | Closed` override fun offerSelectInternal(element: E, select: SelectInstance<*>): Any { - bufferLock.withLock { + state.withLock { // check if closed for send (under lock, so size cannot change) closedForSend?.let { return it } val size = this.size @@ -118,7 +118,7 @@ internal class ArrayBroadcastChannel( return ALREADY_SELECTED } val tail = this.tail - buffer[(tail % capacity).toInt()] = element + state.setBufferAt((tail % capacity).toInt(), element) this.size = size + 1 this.tail = tail + 1 } @@ -143,7 +143,7 @@ internal class ArrayBroadcastChannel( private tailrec fun updateHead(addSub: Subscriber? = null, removeSub: Subscriber? = null) { // update head in a tail rec loop var send: Send? = null - bufferLock.withLock { + state.withLock { if (addSub != null) { addSub.subHead = tail // start from last element val wasEmpty = subscribers.isEmpty() @@ -162,7 +162,7 @@ internal class ArrayBroadcastChannel( var size = this.size // clean up removed (on not need if we don't have any subscribers anymore) while (head < targetHead) { - buffer[(head % capacity).toInt()] = null + state.setBufferAt((head % capacity).toInt(), null) val wasFull = size >= capacity // update the size before checking queue (no more senders can queue up) this.head = ++head @@ -175,7 +175,7 @@ internal class ArrayBroadcastChannel( if (token != null) { assert { token === RESUME_TOKEN } // put sent element to the buffer - buffer[(tail % capacity).toInt()] = (send as Send).pollResult + state.setBufferAt((tail % capacity).toInt(), (send as Send).pollResult) this.size = size + 1 this.tail = tail + 1 return@withLock // go out of lock to wakeup this sender @@ -200,13 +200,10 @@ internal class ArrayBroadcastChannel( return minHead } - @Suppress("UNCHECKED_CAST") - private fun elementAt(index: Long): E = buffer[(index % capacity).toInt()] as E - private class Subscriber( private val broadcastChannel: ArrayBroadcastChannel ) : AbstractChannel(), ReceiveChannel { - private val subLock = ReentrantLock() + private val subLock = reentrantLock() private val _subHead = atomic(0L) var subHead: Long // guarded by subLock @@ -360,7 +357,8 @@ internal class ArrayBroadcastChannel( } // Get tentative result. This result may be wrong (completely invalid value, including null), // because this subscription might get closed, moving channel's head past this subscription's head. - val result = broadcastChannel.elementAt(subHead) + @Suppress("UNCHECKED_CAST") + val result = broadcastChannel.state.getBufferAt((subHead % broadcastChannel.capacity).toInt()) as E // now check if this subscription was closed val closedSub = this.closedForReceive if (closedSub != null) return closedSub @@ -372,5 +370,7 @@ internal class ArrayBroadcastChannel( // ------ debug ------ override val bufferDebugString: String - get() = "(buffer:capacity=${buffer.size},size=$size)" + get() = state.withLock { + "(buffer:capacity=${state.bufferSize},size=$size)" + } } diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayBufferState.common.kt b/kotlinx-coroutines-core/common/src/channels/ArrayBufferState.common.kt new file mode 100644 index 0000000000..bd522e6e18 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/channels/ArrayBufferState.common.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +internal expect open class ArrayBufferState(initialBufferSize: Int) { + val bufferSize: Int + + fun getBufferAt(index: Int): Any? + fun setBufferAt(index: Int, value: Any?) + + inline fun withLock(block: () -> T): T +} diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt index e26579eff7..9853cbe2ff 100644 --- a/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ArrayChannel.kt @@ -4,7 +4,6 @@ package kotlinx.coroutines.channels -import kotlinx.atomicfu.* import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import kotlinx.coroutines.selects.* @@ -29,51 +28,47 @@ internal open class ArrayChannel( require(capacity >= 1) { "ArrayChannel capacity must be at least 1, but $capacity was specified" } } - private val lock = ReentrantLock() /* - * Guarded by lock. - * Allocate minimum of capacity and 16 to avoid excess memory pressure for large channels when it's not necessary. + * Allocate minimum of capacity and 8 to avoid excess memory pressure for large channels when it's not necessary. */ - private var buffer: Array = arrayOfNulls(min(capacity, 8)) - private var head: Int = 0 - private val size = atomic(0) // Invariant: size <= capacity + private val state = ArrayChannelState(min(capacity, 8)) protected final override val isBufferAlwaysEmpty: Boolean get() = false - protected final override val isBufferEmpty: Boolean get() = size.value == 0 + protected final override val isBufferEmpty: Boolean get() = state.size == 0 protected final override val isBufferAlwaysFull: Boolean get() = false - protected final override val isBufferFull: Boolean get() = size.value == capacity + protected final override val isBufferFull: Boolean get() = state.size == capacity - override val isFull: Boolean get() = lock.withLock { isFullImpl } - override val isEmpty: Boolean get() = lock.withLock { isEmptyImpl } - override val isClosedForReceive: Boolean get() = lock.withLock { super.isClosedForReceive } + override val isFull: Boolean get() = state.withLock { isFullImpl } + override val isEmpty: Boolean get() = state.withLock { isEmptyImpl } + override val isClosedForReceive: Boolean get() = state.withLock { super.isClosedForReceive } // result is `OFFER_SUCCESS | OFFER_FAILED | Closed` protected override fun offerInternal(element: E): Any { var receive: ReceiveOrClosed? = null - lock.withLock { - val size = this.size.value + state.withLock { + val size = state.size closedForSend?.let { return it } if (size < capacity) { // tentatively put element to buffer - this.size.value = size + 1 // update size before checking queue (!!!) + state.size = size + 1 // update size before checking queue (!!!) // check for receivers that were waiting on empty queue if (size == 0) { loop@ while (true) { receive = takeFirstReceiveOrPeekClosed() ?: break@loop // break when no receivers queued if (receive is Closed) { - this.size.value = size // restore size + state.size = size // restore size return receive!! } val token = receive!!.tryResumeReceive(element, null) if (token != null) { assert { token === RESUME_TOKEN } - this.size.value = size // restore size + state.size = size // restore size return@withLock } } } - ensureCapacity(size) - buffer[(head + size) % buffer.size] = element // actually queue element + state.ensureCapacity(size, capacity) + state.setBufferAt((state.head + size) % state.bufferSize, element) // actually queue element return OFFER_SUCCESS } // size == capacity: full @@ -87,12 +82,12 @@ internal open class ArrayChannel( // result is `ALREADY_SELECTED | OFFER_SUCCESS | OFFER_FAILED | Closed` protected override fun offerSelectInternal(element: E, select: SelectInstance<*>): Any { var receive: ReceiveOrClosed? = null - lock.withLock { - val size = this.size.value + state.withLock { + val size = state.size closedForSend?.let { return it } if (size < capacity) { // tentatively put element to buffer - this.size.value = size + 1 // update size before checking queue (!!!) + state.size = size + 1 // update size before checking queue (!!!) // check for receivers that were waiting on empty queue if (size == 0) { loop@ while (true) { @@ -100,14 +95,14 @@ internal open class ArrayChannel( val failure = select.performAtomicTrySelect(offerOp) when { failure == null -> { // offered successfully - this.size.value = size // restore size + state.size = size // restore size receive = offerOp.result return@withLock } failure === OFFER_FAILED -> break@loop // cannot offer -> Ok to queue to buffer failure === RETRY_ATOMIC -> {} // retry failure === ALREADY_SELECTED || failure is Closed<*> -> { - this.size.value = size // restore size + state.size = size // restore size return failure } else -> error("performAtomicTrySelect(describeTryOffer) returned $failure") @@ -116,11 +111,11 @@ internal open class ArrayChannel( } // let's try to select sending this element to buffer if (!select.trySelect()) { // :todo: move trySelect completion outside of lock - this.size.value = size // restore size + state.size = size // restore size return ALREADY_SELECTED } - ensureCapacity(size) - buffer[(head + size) % buffer.size] = element // actually queue element + state.ensureCapacity(size, capacity) + state.setBufferAt((state.head + size) % state.bufferSize, element) // actually queue element return OFFER_SUCCESS } // size == capacity: full @@ -131,40 +126,28 @@ internal open class ArrayChannel( return receive!!.offerResult } - override fun enqueueSend(send: Send): Any? = lock.withLock { + override fun enqueueSend(send: Send): Any? = state.withLock { super.enqueueSend(send) } - // Guarded by lock - private fun ensureCapacity(currentSize: Int) { - if (currentSize >= buffer.size) { - val newSize = min(buffer.size * 2, capacity) - val newBuffer = arrayOfNulls(newSize) - for (i in 0 until currentSize) { - newBuffer[i] = buffer[(head + i) % buffer.size] - } - buffer = newBuffer - head = 0 - } - } - // result is `E | POLL_FAILED | Closed` protected override fun pollInternal(): Any? { var send: Send? = null var resumed = false var result: Any? = null - lock.withLock { - val size = this.size.value + state.withLock { + val size = state.size if (size == 0) return closedForSend ?: POLL_FAILED // when nothing can be read from buffer // size > 0: not empty -- retrieve element - result = buffer[head] - buffer[head] = null - this.size.value = size - 1 // update size before checking queue (!!!) + result = state.getBufferAt(state.head) + state.setBufferAt(state.head, null) + state.size = size - 1 // update size before checking queue (!!!) // check for senders that were waiting on full queue var replacement: Any? = POLL_FAILED if (size == capacity) { loop@ while (true) { send = takeFirstSendOrPeekClosed() ?: break + disposeQueue { send as? Closed<*> } val token = send!!.tryResumeSend(null) if (token != null) { assert { token === RESUME_TOKEN } @@ -175,10 +158,10 @@ internal open class ArrayChannel( } } if (replacement !== POLL_FAILED && replacement !is Closed<*>) { - this.size.value = size // restore size - buffer[(head + size) % buffer.size] = replacement + state.size = size // restore size + state.setBufferAt((state.head + size) % state.bufferSize, replacement) } - head = (head + 1) % buffer.size + state.head = (state.head + 1) % state.bufferSize } // complete send the we're taken replacement from if (resumed) @@ -191,13 +174,13 @@ internal open class ArrayChannel( var send: Send? = null var success = false var result: Any? = null - lock.withLock { - val size = this.size.value + state.withLock { + val size = state.size if (size == 0) return closedForSend ?: POLL_FAILED // size > 0: not empty -- retrieve element - result = buffer[head] - buffer[head] = null - this.size.value = size - 1 // update size before checking queue (!!!) + result = state.getBufferAt(state.head) + state.setBufferAt(state.head, null) + state.size = size - 1 // update size before checking queue (!!!) // check for senders that were waiting on full queue var replacement: Any? = POLL_FAILED if (size == capacity) { @@ -214,8 +197,8 @@ internal open class ArrayChannel( failure === POLL_FAILED -> break@loop // cannot poll -> Ok to take from buffer failure === RETRY_ATOMIC -> {} // retry failure === ALREADY_SELECTED -> { - this.size.value = size // restore size - buffer[head] = result // restore head + state.size = size // restore size + state.setBufferAt(state.head, result) // restore head return failure } failure is Closed<*> -> { @@ -229,17 +212,17 @@ internal open class ArrayChannel( } } if (replacement !== POLL_FAILED && replacement !is Closed<*>) { - this.size.value = size // restore size - buffer[(head + size) % buffer.size] = replacement + state.size = size // restore size + state.setBufferAt((state.head + size) % state.bufferSize, replacement) } else { // failed to poll or is already closed --> let's try to select receiving this element from buffer if (!select.trySelect()) { // :todo: move trySelect completion outside of lock - this.size.value = size // restore size - buffer[head] = result // restore head + state.size = size // restore size + state.setBufferAt(state.head, result) // restore head return ALREADY_SELECTED } } - head = (head + 1) % buffer.size + state.head = (state.head + 1) % state.bufferSize } // complete send the we're taken replacement from if (success) @@ -247,7 +230,7 @@ internal open class ArrayChannel( return result } - override fun enqueueReceiveInternal(receive: Receive): Boolean = lock.withLock { + override fun enqueueReceiveInternal(receive: Receive): Boolean = state.withLock { super.enqueueReceiveInternal(receive) } @@ -255,12 +238,12 @@ internal open class ArrayChannel( override fun onCancelIdempotent(wasClosed: Boolean) { // clear buffer first, but do not wait for it in helpers if (wasClosed) { - lock.withLock { - repeat(size.value) { - buffer[head] = 0 - head = (head + 1) % buffer.size + state.withLock { + repeat(state.size) { + state.setBufferAt(state.head, null) + state.head = (state.head + 1) % state.bufferSize } - size.value = 0 + state.size = 0 } } // then clean all queued senders @@ -270,5 +253,7 @@ internal open class ArrayChannel( // ------ debug ------ override val bufferDebugString: String - get() = "(buffer:capacity=$capacity,size=${size.value})" + get() = state.withLock { + "(buffer:capacity=$capacity,size=${state.size})" + } } diff --git a/kotlinx-coroutines-core/common/src/channels/ArrayChannelState.common.kt b/kotlinx-coroutines-core/common/src/channels/ArrayChannelState.common.kt new file mode 100644 index 0000000000..682d27641f --- /dev/null +++ b/kotlinx-coroutines-core/common/src/channels/ArrayChannelState.common.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +internal expect class ArrayChannelState(initialBufferSize: Int) : ArrayBufferState { + var head: Int + var size: Int // Invariant: size <= capacity + + fun ensureCapacity(currentSize: Int, capacity: Int) +} diff --git a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt index 863d1387fc..e99c771054 100644 --- a/kotlinx-coroutines-core/common/src/channels/Broadcast.kt +++ b/kotlinx-coroutines-core/common/src/channels/Broadcast.kt @@ -7,10 +7,7 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel.Factory.CONFLATED import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED -import kotlinx.coroutines.intrinsics.* import kotlin.coroutines.* -import kotlin.coroutines.intrinsics.* -import kotlin.native.concurrent.* /** * Broadcasts all elements of the channel. @@ -170,7 +167,7 @@ private class LazyBroadcastCoroutine( channel: BroadcastChannel, block: suspend ProducerScope.() -> Unit ) : BroadcastCoroutine(parentContext, channel, active = false) { - private val continuation = block.createCoroutineUnintercepted(this, this) + private val saved = saveLazyCoroutine(this, this, block) override fun openSubscription(): ReceiveChannel { // open subscription _first_ @@ -181,6 +178,6 @@ private class LazyBroadcastCoroutine( } override fun onStart() { - continuation.startCoroutineCancellable(this) + startLazyCoroutine(saved, this, this) } } diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt index 4734766914..b358c195ac 100644 --- a/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedChannel.kt @@ -7,8 +7,13 @@ package kotlinx.coroutines.channels import kotlinx.coroutines.* import kotlinx.coroutines.internal.* import kotlinx.coroutines.selects.* +import kotlin.jvm.* import kotlin.native.concurrent.* +@JvmField +@SharedImmutable +internal val EMPTY = Symbol("EMPTY") + /** * Channel that buffers at most one element and conflates all subsequent `send` and `offer` invocations, * so that the receiver always gets the most recently sent element. @@ -20,27 +25,21 @@ import kotlin.native.concurrent.* */ internal open class ConflatedChannel : AbstractChannel() { protected final override val isBufferAlwaysEmpty: Boolean get() = false - protected final override val isBufferEmpty: Boolean get() = value === EMPTY + protected final override val isBufferEmpty: Boolean get() = state.value === EMPTY protected final override val isBufferAlwaysFull: Boolean get() = false protected final override val isBufferFull: Boolean get() = false - override val isEmpty: Boolean get() = lock.withLock { isEmptyImpl } - - private val lock = ReentrantLock() + override val isEmpty: Boolean get() = state.withLock { isEmptyImpl } - private var value: Any? = EMPTY - - private companion object { - private val EMPTY = Symbol("EMPTY") - } + private val state = ConflatedChannelState() // result is `OFFER_SUCCESS | Closed` protected override fun offerInternal(element: E): Any { var receive: ReceiveOrClosed? = null - lock.withLock { + state.withLock { closedForSend?.let { return it } // if there is no element written in buffer - if (value === EMPTY) { + if (state.value === EMPTY) { // check for receivers that were waiting on the empty buffer loop@ while(true) { receive = takeFirstReceiveOrPeekClosed() ?: break@loop // break when no receivers queued @@ -54,7 +53,7 @@ internal open class ConflatedChannel : AbstractChannel() { } } } - value = element + state.value = element return OFFER_SUCCESS } // breaks here if offer meets receiver @@ -65,9 +64,9 @@ internal open class ConflatedChannel : AbstractChannel() { // result is `ALREADY_SELECTED | OFFER_SUCCESS | Closed` protected override fun offerSelectInternal(element: E, select: SelectInstance<*>): Any { var receive: ReceiveOrClosed? = null - lock.withLock { + state.withLock { closedForSend?.let { return it } - if (value === EMPTY) { + if (state.value === EMPTY) { loop@ while(true) { val offerOp = describeTryOffer(element) val failure = select.performAtomicTrySelect(offerOp) @@ -87,7 +86,7 @@ internal open class ConflatedChannel : AbstractChannel() { if (!select.trySelect()) { return ALREADY_SELECTED } - value = element + state.value = element return OFFER_SUCCESS } // breaks here if offer meets receiver @@ -98,10 +97,10 @@ internal open class ConflatedChannel : AbstractChannel() { // result is `E | POLL_FAILED | Closed` protected override fun pollInternal(): Any? { var result: Any? = null - lock.withLock { - if (value === EMPTY) return closedForSend ?: POLL_FAILED - result = value - value = EMPTY + state.withLock { + if (state.value === EMPTY) return closedForSend ?: POLL_FAILED + result = state.value + state.value = EMPTY } return result } @@ -109,31 +108,31 @@ internal open class ConflatedChannel : AbstractChannel() { // result is `E | POLL_FAILED | Closed` protected override fun pollSelectInternal(select: SelectInstance<*>): Any? { var result: Any? = null - lock.withLock { - if (value === EMPTY) return closedForSend ?: POLL_FAILED + state.withLock { + if (state.value === EMPTY) return closedForSend ?: POLL_FAILED if (!select.trySelect()) return ALREADY_SELECTED - result = value - value = EMPTY + result = state.value + state.value = EMPTY } return result } protected override fun onCancelIdempotent(wasClosed: Boolean) { if (wasClosed) { - lock.withLock { - value = EMPTY + state.withLock { + state.value = EMPTY } } super.onCancelIdempotent(wasClosed) } - override fun enqueueReceiveInternal(receive: Receive): Boolean = lock.withLock { + override fun enqueueReceiveInternal(receive: Receive): Boolean = state.withLock { super.enqueueReceiveInternal(receive) } // ------ debug ------ override val bufferDebugString: String - get() = "(value=$value)" + get() = "(value=${state.value})" } diff --git a/kotlinx-coroutines-core/common/src/channels/ConflatedChannelState.common.kt b/kotlinx-coroutines-core/common/src/channels/ConflatedChannelState.common.kt new file mode 100644 index 0000000000..c9b7138790 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/channels/ConflatedChannelState.common.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +internal expect class ConflatedChannelState() { + var value: Any? + inline fun withLock(block: () -> T): T +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt index b2bbb6d3ae..4416d8e94c 100644 --- a/kotlinx-coroutines-core/common/src/flow/StateFlow.kt +++ b/kotlinx-coroutines-core/common/src/flow/StateFlow.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines.flow import kotlinx.atomicfu.* +import kotlinx.atomicfu.locks.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.internal.* diff --git a/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt b/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt index 1836a5284d..5149d0de1a 100644 --- a/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt +++ b/kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt @@ -14,11 +14,4 @@ internal typealias SubscribersList = MutableList internal expect fun subscriberList(): SubscribersList -internal expect class ReentrantLock() { - fun tryLock(): Boolean - fun unlock(): Unit -} - -internal expect inline fun ReentrantLock.withLock(action: () -> T): T - internal expect fun identitySet(expectedSize: Int): MutableSet diff --git a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt index 32258ba101..f42408d5ce 100644 --- a/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt +++ b/kotlinx-coroutines-core/common/src/internal/DispatchedTask.kt @@ -35,7 +35,7 @@ internal abstract class DispatchedTask( val taskContext = this.taskContext var fatalException: Throwable? = null try { - val delegate = delegate as DispatchedContinuation + val delegate = delegate.useLocal() as DispatchedContinuation // cast must succeed val continuation = delegate.continuation val context = continuation.context val state = takeState() // NOTE: Must take state in any case, even if cancelled @@ -96,25 +96,24 @@ internal abstract class DispatchedTask( } } -internal fun DispatchedTask.dispatch(mode: Int) { +internal fun CancellableContinuationImpl.dispatch(mode: Int) { val delegate = this.delegate - if (mode.isDispatchedMode && delegate is DispatchedContinuation<*> && mode.isCancellableMode == resumeMode.isCancellableMode) { + val local = delegate.asLocalOrNull() // go to shareableResume when called from wrong worker + if (mode.isDispatchedMode && local is DispatchedContinuation<*> && mode.isCancellableMode == resumeMode.isCancellableMode) { // dispatch directly using this instance's Runnable implementation - val dispatcher = delegate.dispatcher - val context = delegate.context + val dispatcher = local.dispatcher + val context = local.context if (dispatcher.isDispatchNeeded(context)) { dispatcher.dispatch(context, this) } else { resumeUnconfined() } } else { - resume(delegate, mode) + shareableResume(delegate, mode) } } -@Suppress("UNCHECKED_CAST") -internal fun DispatchedTask.resume(delegate: Continuation, useMode: Int) { - // slow-path - use delegate +internal fun CancellableContinuationImpl.resumeImpl(delegate: Continuation, useMode: Int) { val state = takeState() val exception = getExceptionalResult(state)?.let { recoverStackTrace(it, delegate) } val result = if (exception != null) Result.failure(exception) else Result.success(getSuccessfulResult(state)) @@ -126,7 +125,7 @@ internal fun DispatchedTask.resume(delegate: Continuation, useMode: In } } -private fun DispatchedTask<*>.resumeUnconfined() { +private fun CancellableContinuationImpl.resumeUnconfined() { val eventLoop = ThreadLocalEventLoop.eventLoop if (eventLoop.isUnconfinedLoopActive) { // When unconfined loop is active -- dispatch continuation for execution to avoid stack overflow @@ -134,7 +133,7 @@ private fun DispatchedTask<*>.resumeUnconfined() { } else { // Was not active -- run event loop until all unconfined tasks are executed runUnconfinedEventLoop(eventLoop) { - resume(delegate, MODE_UNDISPATCHED) + resumeImpl(delegate.useLocal(), MODE_UNDISPATCHED) } } } diff --git a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt index f1663c3ddc..6e27b79292 100644 --- a/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt +++ b/kotlinx-coroutines-core/common/src/internal/LockFreeLinkedList.common.kt @@ -69,7 +69,7 @@ public expect open class RemoveFirstDesc(queue: LockFreeLinkedListNode): Abst public expect abstract class AbstractAtomicDesc : AtomicDesc { final override fun prepare(op: AtomicOp<*>): Any? final override fun complete(op: AtomicOp<*>, failure: Any?) - protected open fun failure(affected: LockFreeLinkedListNode): Any? + protected open fun failure(affected: LockFreeLinkedListNode?): Any? // must fail on null for unlinked nodes on K/N protected open fun retry(affected: LockFreeLinkedListNode, next: Any): Boolean public abstract fun finishPrepare(prepareOp: PrepareOp) // non-null on failure public open fun onPrepare(prepareOp: PrepareOp): Any? // non-null on failure diff --git a/kotlinx-coroutines-core/common/src/internal/ManualMemoryManagement.common.kt b/kotlinx-coroutines-core/common/src/internal/ManualMemoryManagement.common.kt new file mode 100644 index 0000000000..7bb2e532d9 --- /dev/null +++ b/kotlinx-coroutines-core/common/src/internal/ManualMemoryManagement.common.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +internal expect inline fun disposeLockFreeLinkedList(list: () -> LockFreeLinkedListNode?) // only needed on Kotlin/Native +internal expect inline fun storeCyclicRef(block: () -> Unit) // nop on native diff --git a/kotlinx-coroutines-core/common/src/internal/Scopes.kt b/kotlinx-coroutines-core/common/src/internal/Scopes.kt index 9bb2ce3d29..dbdf397007 100644 --- a/kotlinx-coroutines-core/common/src/internal/Scopes.kt +++ b/kotlinx-coroutines-core/common/src/internal/Scopes.kt @@ -6,7 +6,6 @@ package kotlinx.coroutines.internal import kotlinx.coroutines.* import kotlin.coroutines.* -import kotlin.coroutines.intrinsics.* import kotlin.jvm.* /** @@ -14,9 +13,11 @@ import kotlin.jvm.* */ internal open class ScopeCoroutine( context: CoroutineContext, - @JvmField val uCont: Continuation // unintercepted continuation + uCont: Continuation ) : AbstractCoroutine(context, true), CoroutineStackFrame { - final override val callerFrame: CoroutineStackFrame? get() = uCont as CoroutineStackFrame? + @JvmField + val uCont: Continuation = uCont.asShareable() // unintercepted continuation, shareable + final override val callerFrame: CoroutineStackFrame? get() = uCont.asLocal() as CoroutineStackFrame? final override fun getStackTraceElement(): StackTraceElement? = null final override val isScopedCoroutine: Boolean get() = true @@ -24,7 +25,7 @@ internal open class ScopeCoroutine( override fun afterCompletion(state: Any?) { // Resume in a cancellable way by default when resuming from another context - uCont.intercepted().resumeCancellableWith(recoverResult(state, uCont)) + uCont.shareableInterceptedResumeCancellableWith(recoverResult(state, uCont)) } override fun afterResume(state: Any?) { diff --git a/kotlinx-coroutines-core/common/src/internal/Sharing.common.kt b/kotlinx-coroutines-core/common/src/internal/Sharing.common.kt new file mode 100644 index 0000000000..ce27a9122f --- /dev/null +++ b/kotlinx-coroutines-core/common/src/internal/Sharing.common.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlinx.coroutines.* +import kotlin.coroutines.* + +internal expect open class ShareableRefHolder() +internal expect fun ShareableRefHolder.disposeSharedRef() +internal expect fun T.asShareable(): DisposableHandle where T : DisposableHandle, T : ShareableRefHolder +internal expect fun CoroutineDispatcher.asShareable(): CoroutineDispatcher +internal expect fun Continuation.asShareable() : Continuation +internal expect fun Continuation.asLocal() : Continuation +internal expect fun Continuation.asLocalOrNull() : Continuation? +internal expect fun Continuation.asLocalOrNullIfNotUsed() : Continuation? +internal expect fun Continuation.useLocal() : Continuation +internal expect fun Continuation.shareableInterceptedResumeCancellableWith(result: Result) +internal expect fun Continuation.shareableInterceptedResumeWith(result: Result) +internal expect fun disposeContinuation(cont: () -> Continuation<*>) +internal expect fun CancellableContinuationImpl.shareableResume(delegate: Continuation, useMode: Int) + +internal expect fun (suspend (T) -> R).asShareable(): suspend (T) -> R + +internal expect fun isReuseSupportedInPlatform(): Boolean +internal expect fun ArrayList.addOrUpdate(element: T, update: (ArrayList) -> Unit) +internal expect fun ArrayList.addOrUpdate(index: Int, element: T, update: (ArrayList) -> Unit) +internal expect fun Any.weakRef(): Any +internal expect fun Any?.unweakRef(): Any? diff --git a/kotlinx-coroutines-core/common/src/internal/Synchronized.common.kt b/kotlinx-coroutines-core/common/src/internal/Synchronized.common.kt deleted file mode 100644 index 3afc7e1802..0000000000 --- a/kotlinx-coroutines-core/common/src/internal/Synchronized.common.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.internal - -import kotlinx.coroutines.* - -/** - * @suppress **This an internal API and should not be used from general code.** - */ -@InternalCoroutinesApi -public expect open class SynchronizedObject() // marker abstract class - -/** - * @suppress **This an internal API and should not be used from general code.** - */ -@InternalCoroutinesApi -public expect inline fun synchronized(lock: SynchronizedObject, block: () -> T): T diff --git a/kotlinx-coroutines-core/common/src/internal/ThreadSafeHeap.kt b/kotlinx-coroutines-core/common/src/internal/ThreadSafeHeap.kt index df55c28f13..49689b241b 100644 --- a/kotlinx-coroutines-core/common/src/internal/ThreadSafeHeap.kt +++ b/kotlinx-coroutines-core/common/src/internal/ThreadSafeHeap.kt @@ -5,6 +5,7 @@ package kotlinx.coroutines.internal import kotlinx.atomicfu.* +import kotlinx.atomicfu.locks.* import kotlinx.coroutines.* /** diff --git a/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt b/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt index 525e322f08..ae3db0c54a 100644 --- a/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt +++ b/kotlinx-coroutines-core/common/src/intrinsics/Undispatched.kt @@ -127,6 +127,8 @@ private inline fun ScopeCoroutine.undispatchedResult( if (result === COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED // (1) val state = makeCompletingOnce(result) if (state === COMPLETING_WAITING_CHILDREN) return COROUTINE_SUSPENDED // (2) + // When scope coroutine does not suspend on Kotlin/Native it shall dispose its continuation which it will not use + disposeContinuation { uCont } return if (state is CompletedExceptionally) { // (3) when { shouldThrow(state.cause) -> throw recoverStackTrace(state.cause, uCont) diff --git a/kotlinx-coroutines-core/common/src/selects/Select.kt b/kotlinx-coroutines-core/common/src/selects/Select.kt index 0595341f92..1840d889ba 100644 --- a/kotlinx-coroutines-core/common/src/selects/Select.kt +++ b/kotlinx-coroutines-core/common/src/selects/Select.kt @@ -233,10 +233,12 @@ private val selectOpSequenceNumber = SeqNumber() @PublishedApi internal class SelectBuilderImpl( - private val uCont: Continuation // unintercepted delegate continuation + uCont: Continuation ) : LockFreeLinkedListHead(), SelectBuilder, SelectInstance, Continuation, CoroutineStackFrame { + private val uCont: Continuation = uCont.asShareable() // unintercepted delegate continuation, shareable + override val callerFrame: CoroutineStackFrame? get() = uCont as? CoroutineStackFrame @@ -305,7 +307,7 @@ internal class SelectBuilderImpl( // Resumes in dispatched way so that it can be called from an arbitrary context override fun resumeSelectWithException(exception: Throwable) { doResume({ CompletedExceptionally(recoverStackTrace(exception, uCont)) }) { - uCont.intercepted().resumeWith(Result.failure(exception)) + uCont.shareableInterceptedResumeWith(Result.failure(exception)) } } @@ -346,16 +348,19 @@ internal class SelectBuilderImpl( internal fun handleBuilderException(e: Throwable) { if (trySelect()) { resumeWithException(e) - } else if (e !is CancellationException) { - /* - * Cannot handle this exception -- builder was already resumed with a different exception, - * so treat it as "unhandled exception". But only if it is not the completion reason - * and it's not the cancellation. Otherwise, in the face of structured concurrency - * the same exception will be reported to the global exception handler. - */ - val result = getResult() - if (result !is CompletedExceptionally || unwrap(result.cause) !== unwrap(e)) { - handleCoroutineException(context, e) + } else { + disposeLockFreeLinkedList { this } + if (e !is CancellationException) { + /* + * Cannot handle this exception -- builder was already resumed with a different exception, + * so treat it as "unhandled exception". But only if it is not the completion reason + * and it's not the cancellation. Otherwise, in the face of structured concurrency + * the same exception will be reported to the global exception handler. + */ + val result = getResult() + if (result !is CompletedExceptionally || unwrap(result.cause) !== unwrap(e)) { + handleCoroutineException(context, e) + } } } } @@ -381,10 +386,12 @@ internal class SelectBuilderImpl( } private fun doAfterSelect() { + val parentHandle = _parentHandle.getAndSet(null) parentHandle?.dispose() forEach { - it.handle.dispose() + it.dispose() } + disposeLockFreeLinkedList { this } } override fun trySelect(): Boolean { @@ -653,6 +660,13 @@ internal class SelectBuilderImpl( } private class DisposeNode( - @JvmField val handle: DisposableHandle - ) : LockFreeLinkedListNode() + handle: DisposableHandle + ) : LockFreeLinkedListNode() { + private val _handle = atomic(handle) + + fun dispose() { + val handle = _handle.getAndSet(null) + handle?.dispose() + } + } } diff --git a/kotlinx-coroutines-core/common/src/sync/Mutex.kt b/kotlinx-coroutines-core/common/src/sync/Mutex.kt index 61e046c77a..d5c41a1332 100644 --- a/kotlinx-coroutines-core/common/src/sync/Mutex.kt +++ b/kotlinx-coroutines-core/common/src/sync/Mutex.kt @@ -378,7 +378,7 @@ internal class MutexImpl(locked: Boolean) : Mutex, SelectClause2 { override fun tryResumeLockWaiter(): Any? = if (select.trySelect()) SELECT_SUCCESS else null override fun completeResumeLockWaiter(token: Any) { assert { token === SELECT_SUCCESS } - block.startCoroutine(receiver = mutex, completion = select.completion) + startCoroutine(CoroutineStart.ATOMIC, mutex, select.completion, block) } override fun toString(): String = "LockSelect[$owner, $mutex, $select]" } diff --git a/kotlinx-coroutines-core/common/test/flow/operators/BackgroundFlowTest.kt b/kotlinx-coroutines-core/common/test/flow/operators/BackgroundFlowTest.kt new file mode 100644 index 0000000000..46ac2b7cb8 --- /dev/null +++ b/kotlinx-coroutines-core/common/test/flow/operators/BackgroundFlowTest.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.flow.operators + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlin.test.* + +/** + * Test some flow operators in background thread, + * mostly for the purpose of checking Kotlin/Native implementation. + */ +class BackgroundFlowTest : TestBase() { + @Test + fun testFlowCombine() = runTest { + withContext(Dispatchers.Default) { + val flow = flowOf(1) + val combo = combine(flow, flow) { a, b -> a + b } + assertEquals(2, combo.first()) + } + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt b/kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt new file mode 100644 index 0000000000..ad91dee5a6 --- /dev/null +++ b/kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.coroutines.* + +/** + * Runs a new coroutine and **blocks** the current thread until its completion. + * This function should not be used from a coroutine. It is designed to bridge regular blocking code + * to libraries that are written in suspending style, to be used in `main` functions and in tests. + */ +public expect fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T diff --git a/kotlinx-coroutines-core/concurrent/src/SingleThread.common.kt b/kotlinx-coroutines-core/concurrent/src/SingleThread.common.kt new file mode 100644 index 0000000000..a0b0fca611 --- /dev/null +++ b/kotlinx-coroutines-core/concurrent/src/SingleThread.common.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +/** + * Creates a coroutine execution context using a single thread. + */ +@ExperimentalCoroutinesApi +public expect fun newSingleThreadContext(name: String): SingleThreadDispatcher + +/** + * A coroutine dispatcher that is confined to a single thread. + */ +@ExperimentalCoroutinesApi +public expect abstract class SingleThreadDispatcher : CoroutineDispatcher { + /** + * Closes this coroutine dispatcher and shuts down its thread. + */ + @ExperimentalCoroutinesApi + public abstract fun close() +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt b/kotlinx-coroutines-core/concurrent/src/internal/LockFreeLinkedList.kt similarity index 86% rename from kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt rename to kotlinx-coroutines-core/concurrent/src/internal/LockFreeLinkedList.kt index 29f37dac28..edd86e5859 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/LockFreeLinkedList.kt +++ b/kotlinx-coroutines-core/concurrent/src/internal/LockFreeLinkedList.kt @@ -7,6 +7,8 @@ package kotlinx.coroutines.internal import kotlinx.atomicfu.* import kotlinx.coroutines.* +import kotlin.jvm.* +import kotlin.native.concurrent.* private typealias Node = LockFreeLinkedListNode @@ -20,9 +22,11 @@ internal const val SUCCESS = 1 internal const val FAILURE = 2 @PublishedApi +@SharedImmutable internal val CONDITION_FALSE: Any = Symbol("CONDITION_FALSE") @PublishedApi +@SharedImmutable internal val LIST_EMPTY: Any = Symbol("LIST_EMPTY") /** @suppress **This is unstable API and it is subject to change.** */ @@ -59,18 +63,25 @@ public actual typealias PrepareOp = LockFreeLinkedListNode.PrepareOp @Suppress("LeakingThis") @InternalCoroutinesApi public actual open class LockFreeLinkedListNode { - private val _next = atomic(this) // Node | Removed | OpDescriptor - private val _prev = atomic(this) // Node to the left (cannot be marked as removed) + // those _next & _prev refs can be null on Kotlin/Native when doubly-linked list is unlinked + private val _next = atomic(this) // Node | Removed | OpDescriptor + private val _prev = atomic(this) // Node to the left (cannot be marked as removed) private val _removedRef = atomic(null) // lazily cached removed ref to this private fun removed(): Removed = - _removedRef.value ?: Removed(this).also { _removedRef.lazySet(it) } + _removedRef.value ?: Removed(this).also { + storeCyclicRef { _removedRef.lazySet(it) } + } @PublishedApi internal abstract class CondAddOp( @JvmField val newNode: Node ) : AtomicOp() { - @JvmField var oldNext: Node? = null + private val _oldNext = atomic(null) + + var oldNext: Node? + get() = _oldNext.value + set(value) { _oldNext.value = value } override fun complete(affected: Node, failure: Any?) { val success = failure == null @@ -91,7 +102,8 @@ public actual open class LockFreeLinkedListNode { public actual open val isRemoved: Boolean get() = next is Removed // LINEARIZABLE. Returns Node | Removed - public val next: Any get() { + // Returns null for the unlinked node on Kotlin/Native + public val next: Any? get() { _next.loop { next -> if (next !is OpDescriptor) return next next.perform(this) @@ -99,19 +111,26 @@ public actual open class LockFreeLinkedListNode { } // LINEARIZABLE. Returns next non-removed Node - public actual val nextNode: Node get() = next.unwrap() + public actual val nextNode: Node get() = next?.unwrap() ?: this // it could have been unlinked on Kotlin/Native // LINEARIZABLE WHEN THIS NODE IS NOT REMOVED: // Returns prev non-removed Node, makes sure prev is correct (prev.next === this) // NOTE: if this node is removed, then returns non-removed previous node without applying // prev.next correction, which does not provide linearizable backwards iteration, but can be used to // resume forward iteration when current node was removed. - public actual val prevNode: Node - get() = correctPrev(null) ?: findPrevNonRemoved(_prev.value) + // NOTE: It could have been unlinked on Kotlin/Native. In this case `this` is returned. + public actual val prevNode: Node get() = prevOrNull ?: this + + // Just line prevNode, but return nulls for unlinked nodes on Kotlin/Native + @PublishedApi + internal val prevOrNull: Node? + get() { + return correctPrev(null) ?: findPrevNonRemoved(_prev.value ?: return null) + } - private tailrec fun findPrevNonRemoved(current: Node): Node { + private tailrec fun findPrevNonRemoved(current: Node): Node? { if (!current.isRemoved) return current - return findPrevNonRemoved(current._prev.value) + return findPrevNonRemoved(current._prev.value ?: return null) } // ------ addOneIfEmpty ------ @@ -137,7 +156,8 @@ public actual open class LockFreeLinkedListNode { */ public actual fun addLast(node: Node) { while (true) { // lock-free loop on prev.next - if (prevNode.addNext(node, this)) return + val prev = prevOrNull ?: return // can be unlinked on Kotlin/Native + if (prev.addNext(node, this)) return } } @@ -149,7 +169,8 @@ public actual open class LockFreeLinkedListNode { public actual inline fun addLastIf(node: Node, crossinline condition: () -> Boolean): Boolean { val condAdd = makeCondAddOp(node, condition) while (true) { // lock-free loop on prev.next - val prev = prevNode // sentinel node is never removed, so prev is always defined + // sentinel node is never removed, so prev is always defined, but can be concurrently unlinked on Kotlin/Native + val prev = prevOrNull ?: return false when (prev.tryCondAddNext(node, this, condAdd)) { SUCCESS -> return true FAILURE -> return false @@ -159,7 +180,8 @@ public actual open class LockFreeLinkedListNode { public actual inline fun addLastIfPrev(node: Node, predicate: (Node) -> Boolean): Boolean { while (true) { // lock-free loop on prev.next - val prev = prevNode // sentinel node is never removed, so prev is always defined + // sentinel node is never removed, so prev is always defined, but can be unlinked on Kotlin/Native + val prev = prevOrNull ?: return false if (!predicate(prev)) return false if (prev.addNext(node, this)) return true } @@ -172,7 +194,8 @@ public actual open class LockFreeLinkedListNode { ): Boolean { val condAdd = makeCondAddOp(node, condition) while (true) { // lock-free loop on prev.next - val prev = prevNode // sentinel node is never removed, so prev is always defined + // sentinel node is never removed, so prev is always defined, but can be unlinked on Kotlin/Native + val prev = prevOrNull ?: return false if (!predicate(prev)) return false when (prev.tryCondAddNext(node, this, condAdd)) { SUCCESS -> return true @@ -240,13 +263,16 @@ public actual open class LockFreeLinkedListNode { public actual open fun remove(): Boolean = removeOrNext() == null - // returns null if removed successfully or next node if this node is already removed + // returns null if removed successfully or next node if this node is already removed or unlinked on Kotlin/Native @PublishedApi internal fun removeOrNext(): Node? { while (true) { // lock-free loop on next - val next = this.next + val next = this.next ?: return null // abort when unlinked on Kotlin/Native if (next is Removed) return next.ref // was already removed -- don't try to help (original thread will take care) - if (next === this) return next // was not even added + if (next === this) { + _prev.value = null // Unlink this node from itself to avoid cycle on Kotlin/Native + return next // was not even added + } val removed = (next as Node).removed() if (_next.compareAndSet(next, removed)) { // was removed successfully (linearized remove) -- fixup the list @@ -259,7 +285,7 @@ public actual open class LockFreeLinkedListNode { // Helps with removal of this node public actual fun helpRemove() { // Note: this node must be already removed - (next as Removed).ref.correctPrev(null) + (next as Removed?)?.ref?.correctPrev(null) } // Helps with removal of nodes that are previous to this @@ -269,9 +295,9 @@ public actual open class LockFreeLinkedListNode { // called on a removed node. There's always at least one non-removed node (list head). var node = this while (true) { - val next = node.next + val next = node.next ?: return // abort when unlinked on Kotlin/Native if (next !is Removed) break - node = next.ref + node = next.ref ?: return // abort when unlinked on Kotlin/Native } // Found a non-removed node node.correctPrev(null) @@ -371,12 +397,13 @@ public actual open class LockFreeLinkedListNode { final override val originalNext: Node? get() = _originalNext.value // check node predicates here, must signal failure if affect is not of type T - protected override fun failure(affected: Node): Any? = - if (affected === queue) LIST_EMPTY else null + protected override fun failure(affected: Node?): Any? = + if (affected === queue || affected == null) LIST_EMPTY else null // must fail on null for unlinked nodes on K/N final override fun retry(affected: Node, next: Any): Boolean { if (next !is Removed) return false - next.ref.helpRemovePrev() // must help delete to ensure lock-freedom + val nextRef = next.ref ?: return false // abort when unlinked on Kotlin/Native + nextRef.helpRemovePrev() // must help delete to ensure lock-freedom return true } @@ -443,7 +470,7 @@ public actual open class LockFreeLinkedListNode { protected abstract val affectedNode: Node? protected abstract val originalNext: Node? protected open fun takeAffectedNode(op: OpDescriptor): Node? = affectedNode!! // null for RETRY_ATOMIC - protected open fun failure(affected: Node): Any? = null // next: Node | Removed + protected open fun failure(affected: Node?): Any? = null // next: Node | Removed // must fail on null for unlinked nodes on K/N protected open fun retry(affected: Node, next: Any): Boolean = false // next: Node | Removed protected abstract fun updatedNext(affected: Node, next: Node): Any protected abstract fun finishOnSuccess(affected: Node, next: Node) @@ -473,9 +500,11 @@ public actual open class LockFreeLinkedListNode { next.perform(affected) continue // and retry } + // on Kotlin/Native next can be already unlinked // next: Node | Removed - val failure = failure(affected) + val failure = failure(affected) // must fail on null for unlinked nodes on K/N if (failure != null) return failure // signal failure + next as Any // should have failed if was null if (retry(affected, next)) continue // retry operation val prepareOp = PrepareOp(affected, next as Node, this) if (affected._next.compareAndSet(next, prepareOp)) { @@ -547,7 +576,7 @@ public actual open class LockFreeLinkedListNode { * Returns the corrected value of the previous node while also correcting the `prev` pointer * (so that `this.prev.next === this`) and helps complete node removals to the left ot this node. * - * It returns `null` in two special cases: + * It returns `null` in special cases: * * * When this node is removed. In this case there is no need to waste time on corrections, because * remover of this node will ultimately call [correctPrev] on the next node and that will fix all @@ -555,13 +584,14 @@ public actual open class LockFreeLinkedListNode { * * When [op] descriptor is not `null` and operation descriptor that is [OpDescriptor.isEarlierThan] * that current [op] is found while traversing the list. This `null` result will be translated * by callers to [RETRY_ATOMIC]. + * * When list is unlinked on Kotlin/Native. */ private tailrec fun correctPrev(op: OpDescriptor?): Node? { val oldPrev = _prev.value - var prev: Node = oldPrev + var prev: Node = oldPrev ?: return null // abort when unlinked on Kotlin/Native var last: Node? = null // will be set so that last.next === prev while (true) { // move the left until first non-removed node - val prevNext: Any = prev._next.value + val prevNext: Any = prev._next.value ?: return null // abort when unlinked on Kotlin/Native when { // fast path to find quickly find prev node when everything is properly linked prevNext === this -> { @@ -591,7 +621,7 @@ public actual open class LockFreeLinkedListNode { prev = last last = null } else { - prev = prev._prev.value + prev = prev._prev.value ?: return null // abort when unlinked on Kotlin/Native } } else -> { // prevNext is a regular node, but not this -- help delete @@ -607,10 +637,20 @@ public actual open class LockFreeLinkedListNode { assert { next === this._next.value } } - override fun toString(): String = "${this::class.java.simpleName}@${Integer.toHexString(System.identityHashCode(this))}" + /** + * Only needed on Kotlin/Native to unlink cyclic data structure. See [disposeLockFreeLinkedList]. + */ + internal fun unlinkRefs(last: Boolean) { + if (last) _next.value = null + _prev.value = null + } + + override fun toString(): String = "$classSimpleName@$hexAddress" } -private class Removed(@JvmField val ref: Node) { +private class Removed(ref: Node) { + private val wRef: Any = ref.weakRef() + val ref: Node? get() = wRef.unweakRef() as Node? override fun toString(): String = "Removed[$ref]" } @@ -629,10 +669,10 @@ public actual open class LockFreeLinkedListHead : LockFreeLinkedListNode() { * Iterates over all elements in this list of a specified type. */ public actual inline fun forEach(block: (T) -> Unit) { - var cur: Node = next as Node - while (cur != this) { + var cur: Node? = next as Node? + while (cur != this && cur != null) { if (cur is T) block(cur) - cur = cur.nextNode + cur = cur.next?.unwrap() } } diff --git a/kotlinx-coroutines-core/concurrent/test/StressUtil.common.kt b/kotlinx-coroutines-core/concurrent/test/StressUtil.common.kt new file mode 100644 index 0000000000..6310c9e474 --- /dev/null +++ b/kotlinx-coroutines-core/concurrent/test/StressUtil.common.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +expect fun randomWait() diff --git a/kotlinx-coroutines-core/concurrent/test/exceptions/ConcurrentExceptionsStressTest.kt b/kotlinx-coroutines-core/concurrent/test/exceptions/ConcurrentExceptionsStressTest.kt new file mode 100644 index 0000000000..0a5a209404 --- /dev/null +++ b/kotlinx-coroutines-core/concurrent/test/exceptions/ConcurrentExceptionsStressTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.exceptions + +import kotlinx.coroutines.* +import kotlin.test.* + +class ConcurrentExceptionsStressTest : TestBase() { + private val nWorkers = 4 + private val nRepeat = 1000 * stressTestMultiplier + + private val workers = Array(nWorkers) { index -> + newSingleThreadContext("JobExceptionsStressTest-$index") + } + + @AfterTest + fun tearDown() { + workers.forEach { + it.close() + } + } + + @Test + @Ignore // todo: this test is leaking memory on Kotlin/Native + fun testStress() = runTest { + repeat(nRepeat) { + testOnce() + } + } + + @Suppress("SuspendFunctionOnCoroutineScope") // workaround native inline fun stacktraces + private suspend fun CoroutineScope.testOnce() { + val deferred = async(NonCancellable) { + repeat(nWorkers) { index -> + // Always launch a coroutine even if parent job was already cancelled (atomic start) + launch(workers[index], start = CoroutineStart.ATOMIC) { + randomWait() + throw StressException(index) + } + } + } + deferred.join() + assertTrue(deferred.isCancelled) + val completionException = deferred.getCompletionExceptionOrNull() + val cause = completionException as? StressException + ?: unexpectedException("completion", completionException) + val suppressed = cause.suppressedExceptions + val indices = listOf(cause.index) + suppressed.mapIndexed { index, e -> + (e as? StressException)?.index ?: unexpectedException("suppressed $index", e) + } + repeat(nWorkers) { index -> + assertTrue(index in indices, "Exception $index is missing: $indices") + } + assertEquals(nWorkers, indices.size, "Duplicated exceptions in list: $indices") + } + + private fun unexpectedException(msg: String, e: Throwable?): Nothing { + e?.printStackTrace() + throw IllegalStateException("Unexpected $msg exception", e) + } + + private class StressException(val index: Int) : Throwable() +} + diff --git a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListTest.kt b/kotlinx-coroutines-core/concurrent/test/internal/LockFreeLinkedListTest.kt similarity index 97% rename from kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListTest.kt rename to kotlinx-coroutines-core/concurrent/test/internal/LockFreeLinkedListTest.kt index b9011448cd..dedc3f7f29 100644 --- a/kotlinx-coroutines-core/jvm/test/internal/LockFreeLinkedListTest.kt +++ b/kotlinx-coroutines-core/concurrent/test/internal/LockFreeLinkedListTest.kt @@ -1,10 +1,9 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.internal -import org.junit.Test import kotlin.test.* class LockFreeLinkedListTest { diff --git a/kotlinx-coroutines-core/js/src/Builders.kt b/kotlinx-coroutines-core/js/src/Builders.kt new file mode 100644 index 0000000000..b7ec1430ed --- /dev/null +++ b/kotlinx-coroutines-core/js/src/Builders.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.coroutines.intrinsics.* +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* + +@Suppress("NOTHING_TO_INLINE") // Save an entry on call stack +internal actual inline fun startCoroutine( + start: CoroutineStart, + receiver: R, + completion: Continuation, + noinline block: suspend R.() -> T +) = + startCoroutineImpl(start, receiver, completion, block) + +@Suppress("NOTHING_TO_INLINE") // Save an entry on call stack +internal actual inline fun saveLazyCoroutine( + coroutine: AbstractCoroutine, + receiver: R, + noinline block: suspend R.() -> T +): Any = + block.createCoroutineUnintercepted(receiver, coroutine) + +@Suppress("NOTHING_TO_INLINE", "UNCHECKED_CAST") // Save an entry on call stack +internal actual inline fun startLazyCoroutine( + saved: Any, + coroutine: AbstractCoroutine, + receiver: R +) = + (saved as Continuation).startCoroutineCancellable(coroutine) diff --git a/kotlinx-coroutines-core/js/src/CoroutineContext.kt b/kotlinx-coroutines-core/js/src/CoroutineContext.kt index c0b0c511f9..b0aaf2f928 100644 --- a/kotlinx-coroutines-core/js/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/js/src/CoroutineContext.kt @@ -11,7 +11,7 @@ private external val navigator: dynamic private const val UNDEFINED = "undefined" internal external val process: dynamic -internal actual fun createDefaultDispatcher(): CoroutineDispatcher = when { +internal fun createDefaultDispatcher(): CoroutineDispatcher = when { // Check if we are running under ReactNative. We have to use NodeDispatcher under it. // The problem is that ReactNative has a `window` object with `addEventListener`, but it does not really work. // For details see https://github.com/Kotlin/kotlinx.coroutines/issues/236 diff --git a/kotlinx-coroutines-core/js/src/Dispatchers.kt b/kotlinx-coroutines-core/js/src/Dispatchers.kt index 995801ea0d..6389f5c7fb 100644 --- a/kotlinx-coroutines-core/js/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/js/src/Dispatchers.kt @@ -7,24 +7,19 @@ package kotlinx.coroutines import kotlin.coroutines.* public actual object Dispatchers { - public actual val Default: CoroutineDispatcher = createDefaultDispatcher() - - public actual val Main: MainCoroutineDispatcher = JsMainDispatcher(Default) - + public actual val Main: MainCoroutineDispatcher = JsMainDispatcher(Default, false) public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined } -private class JsMainDispatcher(val delegate: CoroutineDispatcher) : MainCoroutineDispatcher() { - - override val immediate: MainCoroutineDispatcher - get() = throw UnsupportedOperationException("Immediate dispatching is not supported on JS") - +private class JsMainDispatcher( + val delegate: CoroutineDispatcher, + private val invokeImmediately: Boolean +) : MainCoroutineDispatcher() { + override val immediate: MainCoroutineDispatcher = + if (invokeImmediately) this else JsMainDispatcher(delegate, true) + override fun isDispatchNeeded(context: CoroutineContext): Boolean = !invokeImmediately override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block) - - override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context) - override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.dispatchYield(context, block) - override fun toString(): String = delegate.toString() } diff --git a/kotlinx-coroutines-core/js/src/Exceptions.kt b/kotlinx-coroutines-core/js/src/Exceptions.kt index c82199a429..a1ea1d989d 100644 --- a/kotlinx-coroutines-core/js/src/Exceptions.kt +++ b/kotlinx-coroutines-core/js/src/Exceptions.kt @@ -25,8 +25,9 @@ public actual open class CancellationException( internal actual class JobCancellationException public actual constructor( message: String, cause: Throwable?, - internal actual val job: Job + job: Job ) : CancellationException(message, cause) { + internal actual val job: Job? = job override fun toString(): String = "${super.toString()}; job=$job" override fun equals(other: Any?): Boolean = other === this || diff --git a/kotlinx-coroutines-core/js/src/channels/ArrayBufferState.kt b/kotlinx-coroutines-core/js/src/channels/ArrayBufferState.kt new file mode 100644 index 0000000000..0b985c886b --- /dev/null +++ b/kotlinx-coroutines-core/js/src/channels/ArrayBufferState.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +internal actual open class ArrayBufferState actual constructor(initialBufferSize: Int) { + protected var buffer: Array = arrayOfNulls(initialBufferSize) + + actual val bufferSize: Int get() = buffer.size + + actual fun getBufferAt(index: Int): Any? = + buffer[index] + + actual fun setBufferAt(index: Int, value: Any?) { + buffer[index] = value + } + + actual inline fun withLock(block: () -> T): T = block() +} diff --git a/kotlinx-coroutines-core/js/src/channels/ArrayChannelState.kt b/kotlinx-coroutines-core/js/src/channels/ArrayChannelState.kt new file mode 100644 index 0000000000..4d81aabf03 --- /dev/null +++ b/kotlinx-coroutines-core/js/src/channels/ArrayChannelState.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +import kotlin.math.* + +internal actual class ArrayChannelState actual constructor(initialBufferSize: Int) : ArrayBufferState(initialBufferSize) { + actual var head = 0 + actual var size = 0 + + actual fun ensureCapacity(currentSize: Int, capacity: Int) { + if (currentSize < buffer.size) return + val newSize = min(buffer.size * 2, capacity) + val newBuffer = arrayOfNulls(newSize) + for (i in 0 until currentSize) { + newBuffer[i] = buffer[(head + i) % buffer.size] + } + buffer = newBuffer + head = 0 + } +} diff --git a/kotlinx-coroutines-core/js/src/channels/ConflatedChannelState.kt b/kotlinx-coroutines-core/js/src/channels/ConflatedChannelState.kt new file mode 100644 index 0000000000..91c205aba6 --- /dev/null +++ b/kotlinx-coroutines-core/js/src/channels/ConflatedChannelState.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +internal actual class ConflatedChannelState { + actual var value: Any? = EMPTY + + actual inline fun withLock(block: () -> T): T = block() +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/js/src/internal/Concurrent.kt b/kotlinx-coroutines-core/js/src/internal/Concurrent.kt index 5555137f7f..e6438d5e92 100644 --- a/kotlinx-coroutines-core/js/src/internal/Concurrent.kt +++ b/kotlinx-coroutines-core/js/src/internal/Concurrent.kt @@ -4,15 +4,6 @@ package kotlinx.coroutines.internal -internal actual typealias ReentrantLock = NoOpLock - -internal actual inline fun ReentrantLock.withLock(action: () -> T) = action() - -internal class NoOpLock { - fun tryLock() = true - fun unlock(): Unit {} -} - internal actual fun subscriberList(): SubscribersList = CopyOnWriteList() internal actual fun identitySet(expectedSize: Int): MutableSet = HashSet(expectedSize) diff --git a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt index 342b11c69a..e941276c66 100644 --- a/kotlinx-coroutines-core/js/src/internal/LinkedList.kt +++ b/kotlinx-coroutines-core/js/src/internal/LinkedList.kt @@ -133,7 +133,7 @@ public actual abstract class AbstractAtomicDesc : AtomicDesc() { } actual final override fun complete(op: AtomicOp<*>, failure: Any?) = onComplete() - protected actual open fun failure(affected: LockFreeLinkedListNode): Any? = null // Never fails by default + protected actual open fun failure(affected: LockFreeLinkedListNode?): Any? = null // Never fails by default // must fail on null for unlinked nodes on K/N protected actual open fun retry(affected: LockFreeLinkedListNode, next: Any): Boolean = false // Always succeeds protected actual abstract fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) } diff --git a/kotlinx-coroutines-core/js/src/internal/ManualMemoryManagement.kt b/kotlinx-coroutines-core/js/src/internal/ManualMemoryManagement.kt new file mode 100644 index 0000000000..2df7a8e8ea --- /dev/null +++ b/kotlinx-coroutines-core/js/src/internal/ManualMemoryManagement.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +@Suppress("NOTHING_TO_INLINE") +internal actual inline fun disposeLockFreeLinkedList(list: () -> LockFreeLinkedListNode?) {} // only needed on Kotlin/Native + +@Suppress("NOTHING_TO_INLINE") +internal actual inline fun storeCyclicRef(block: () -> Unit) = block() diff --git a/kotlinx-coroutines-core/js/src/internal/Sharing.kt b/kotlinx-coroutines-core/js/src/internal/Sharing.kt new file mode 100644 index 0000000000..111ec64ecd --- /dev/null +++ b/kotlinx-coroutines-core/js/src/internal/Sharing.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlinx.coroutines.* +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* + +@Suppress("ACTUAL_WITHOUT_EXPECT") // visibility different +internal actual typealias ShareableRefHolder = Any + +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual fun ShareableRefHolder.disposeSharedRef() {} + +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual fun T.asShareable(): DisposableHandle where T : DisposableHandle, T : ShareableRefHolder = this + +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun CoroutineDispatcher.asShareable(): CoroutineDispatcher = this + +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun Continuation.asShareable() : Continuation = this + +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun Continuation.asLocal() : Continuation = this + +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun Continuation.asLocalOrNull() : Continuation? = this + +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual fun Continuation.asLocalOrNullIfNotUsed() : Continuation? = this + +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun Continuation.useLocal() : Continuation = this + +@Suppress("NOTHING_TO_INLINE") // Save an entry on call stack +internal actual inline fun Continuation.shareableInterceptedResumeCancellableWith(result: Result) { + intercepted().resumeCancellableWith(result) +} + +@Suppress("NOTHING_TO_INLINE") // Save an entry on call stack +internal actual inline fun Continuation.shareableInterceptedResumeWith(result: Result) { + intercepted().resumeWith(result) +} + +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun disposeContinuation(cont: () -> Continuation<*>) {} + +@Suppress("NOTHING_TO_INLINE") // Save an entry on call stack +internal actual inline fun CancellableContinuationImpl.shareableResume(delegate: Continuation, useMode: Int) = + resumeImpl(delegate, useMode) + +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun (suspend (T) -> R).asShareable(): suspend (T) -> R = this + +@Suppress("NOTHING_TO_INLINE") // Save an entry on call stack +internal actual inline fun isReuseSupportedInPlatform() = true + +internal actual inline fun ArrayList.addOrUpdate(element: T, update: (ArrayList) -> Unit) { + add(element) +} + +internal actual inline fun ArrayList.addOrUpdate(index: Int, element: T, update: (ArrayList) -> Unit) { + add(index, element) +} + +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun Any.weakRef(): Any = this + +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun Any?.unweakRef(): Any? = this diff --git a/kotlinx-coroutines-core/js/src/internal/Synchronized.kt b/kotlinx-coroutines-core/js/src/internal/Synchronized.kt deleted file mode 100644 index 0911dbe115..0000000000 --- a/kotlinx-coroutines-core/js/src/internal/Synchronized.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.internal - -import kotlinx.coroutines.* - -/** - * @suppress **This an internal API and should not be used from general code.** - */ -@InternalCoroutinesApi -public actual typealias SynchronizedObject = Any - -/** - * @suppress **This an internal API and should not be used from general code.** - */ -@InternalCoroutinesApi -public actual inline fun synchronized(lock: SynchronizedObject, block: () -> T): T = - block() diff --git a/kotlinx-coroutines-core/jvm/src/Builders.kt b/kotlinx-coroutines-core/jvm/src/Builders.kt index b8a250fef6..90ac58c28e 100644 --- a/kotlinx-coroutines-core/jvm/src/Builders.kt +++ b/kotlinx-coroutines-core/jvm/src/Builders.kt @@ -7,8 +7,10 @@ package kotlinx.coroutines +import kotlinx.coroutines.intrinsics.* import java.util.concurrent.locks.* import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* /** * Runs a new coroutine and **blocks** the current thread _interruptibly_ until its completion. @@ -33,7 +35,7 @@ import kotlin.coroutines.* * @param block the coroutine code. */ @Throws(InterruptedException::class) -public fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T { +public actual fun runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T { val currentThread = Thread.currentThread() val contextInterceptor = context[ContinuationInterceptor] val eventLoop: EventLoop? @@ -93,3 +95,28 @@ private class BlockingCoroutine( return state as T } } + +@Suppress("NOTHING_TO_INLINE") // Save an entry on call stack +internal actual inline fun startCoroutine( + start: CoroutineStart, + receiver: R, + completion: Continuation, + noinline block: suspend R.() -> T +) = + startCoroutineImpl(start, receiver, completion, block) + +@Suppress("NOTHING_TO_INLINE") // Save an entry on call stack +internal actual inline fun saveLazyCoroutine( + coroutine: AbstractCoroutine, + receiver: R, + noinline block: suspend R.() -> T +): Any = + block.createCoroutineUnintercepted(receiver, coroutine) + +@Suppress("NOTHING_TO_INLINE", "UNCHECKED_CAST") // Save an entry on call stack +internal actual inline fun startLazyCoroutine( + saved: Any, + coroutine: AbstractCoroutine, + receiver: R +) = + (saved as Continuation).startCoroutineCancellable(coroutine) diff --git a/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt b/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt index 5a69d48aac..f1ae0688db 100644 --- a/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/jvm/src/CoroutineContext.kt @@ -6,7 +6,6 @@ package kotlinx.coroutines import kotlinx.coroutines.internal.* import kotlinx.coroutines.scheduling.* -import java.util.concurrent.atomic.* import kotlin.coroutines.* internal const val COROUTINES_SCHEDULER_PROPERTY_NAME = "kotlinx.coroutines.scheduler" @@ -19,7 +18,7 @@ internal val useCoroutinesScheduler = systemProp(COROUTINES_SCHEDULER_PROPERTY_N } } -internal actual fun createDefaultDispatcher(): CoroutineDispatcher = +internal fun createDefaultDispatcher(): CoroutineDispatcher = if (useCoroutinesScheduler) DefaultScheduler else CommonPool /** diff --git a/kotlinx-coroutines-core/jvm/src/Exceptions.kt b/kotlinx-coroutines-core/jvm/src/Exceptions.kt index 0684ce2397..db3c038db7 100644 --- a/kotlinx-coroutines-core/jvm/src/Exceptions.kt +++ b/kotlinx-coroutines-core/jvm/src/Exceptions.kt @@ -29,8 +29,9 @@ public actual fun CancellationException(message: String?, cause: Throwable?) : C internal actual class JobCancellationException public actual constructor( message: String, cause: Throwable?, - @JvmField internal actual val job: Job + job: Job ) : CancellationException(message), CopyableThrowable { + @JvmField internal actual val job: Job? = job init { if (cause != null) initCause(cause) @@ -52,7 +53,7 @@ internal actual class JobCancellationException public actual constructor( override fun createCopy(): JobCancellationException? { if (DEBUG) { - return JobCancellationException(message!!, this, job) + return JobCancellationException(message!!, this, job!!) } /* diff --git a/kotlinx-coroutines-core/jvm/src/Executors.kt b/kotlinx-coroutines-core/jvm/src/Executors.kt index a4d6b46c43..8c36bfa14f 100644 --- a/kotlinx-coroutines-core/jvm/src/Executors.kt +++ b/kotlinx-coroutines-core/jvm/src/Executors.kt @@ -14,7 +14,7 @@ import kotlin.coroutines.* * Instances of [ExecutorCoroutineDispatcher] should be closed by the owner of the dispatcher. * * This class is generally used as a bridge between coroutine-based API and - * asynchronous API which requires instance of the [Executor]. + * asynchronous API that requires an instance of the [Executor]. */ public abstract class ExecutorCoroutineDispatcher: CoroutineDispatcher(), Closeable { /** @suppress */ diff --git a/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt b/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt index a0e1ffa2c7..81ceca3b05 100644 --- a/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt +++ b/kotlinx-coroutines-core/jvm/src/ThreadPoolDispatcher.kt @@ -4,13 +4,12 @@ package kotlinx.coroutines -import kotlinx.coroutines.internal.* import java.util.concurrent.* -import java.util.concurrent.atomic.AtomicInteger -import kotlin.coroutines.* +import java.util.concurrent.atomic.* /** - * Creates a coroutine execution context using a single thread with built-in [yield] support. + * Creates a coroutine execution context using a single thread. + * * **NOTE: The resulting [ExecutorCoroutineDispatcher] owns native resources (its thread). * Resources are reclaimed by [ExecutorCoroutineDispatcher.close].** * @@ -26,10 +25,15 @@ import kotlin.coroutines.* * * @param name the base name of the created thread. */ -@ObsoleteCoroutinesApi -public fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher = +@ExperimentalCoroutinesApi +public actual fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher = newFixedThreadPoolContext(1, name) +/** + * A coroutine dispatcher that is confined to a single thread. + */ +public actual typealias SingleThreadDispatcher = ExecutorCoroutineDispatcher + /** * Creates a coroutine execution context with the fixed-size thread-pool and built-in [yield] support. * **NOTE: The resulting [ExecutorCoroutineDispatcher] owns native resources (its threads). diff --git a/kotlinx-coroutines-core/jvm/src/channels/ArrayBufferState.kt b/kotlinx-coroutines-core/jvm/src/channels/ArrayBufferState.kt new file mode 100644 index 0000000000..eaefe40db9 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/channels/ArrayBufferState.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +internal actual open class ArrayBufferState actual constructor(initialBufferSize: Int) { + protected var buffer: Array = arrayOfNulls(initialBufferSize) + + actual val bufferSize: Int get() = buffer.size + + actual fun getBufferAt(index: Int): Any? = + buffer[index] + + actual fun setBufferAt(index: Int, value: Any?) { + buffer[index] = value + } + + actual inline fun withLock(block: () -> T): T = + synchronized(this) { + block() + } +} diff --git a/kotlinx-coroutines-core/jvm/src/channels/ArrayChannelState.kt b/kotlinx-coroutines-core/jvm/src/channels/ArrayChannelState.kt new file mode 100644 index 0000000000..4d81aabf03 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/channels/ArrayChannelState.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +import kotlin.math.* + +internal actual class ArrayChannelState actual constructor(initialBufferSize: Int) : ArrayBufferState(initialBufferSize) { + actual var head = 0 + actual var size = 0 + + actual fun ensureCapacity(currentSize: Int, capacity: Int) { + if (currentSize < buffer.size) return + val newSize = min(buffer.size * 2, capacity) + val newBuffer = arrayOfNulls(newSize) + for (i in 0 until currentSize) { + newBuffer[i] = buffer[(head + i) % buffer.size] + } + buffer = newBuffer + head = 0 + } +} diff --git a/kotlinx-coroutines-core/jvm/src/channels/ConflatedChannelState.kt b/kotlinx-coroutines-core/jvm/src/channels/ConflatedChannelState.kt new file mode 100644 index 0000000000..f5db45fbf4 --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/channels/ConflatedChannelState.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +internal actual class ConflatedChannelState { + actual var value: Any? = EMPTY + + actual inline fun withLock(block: () -> T): T = + synchronized(this) { + block() + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt b/kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt index 75b668a335..c6266ae0f9 100644 --- a/kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt +++ b/kotlinx-coroutines-core/jvm/src/internal/Concurrent.kt @@ -7,18 +7,10 @@ package kotlinx.coroutines.internal import java.lang.reflect.* import java.util.* import java.util.concurrent.* -import kotlin.concurrent.withLock as withLockJvm internal actual fun subscriberList(): SubscribersList = CopyOnWriteArrayList() -@Suppress("ACTUAL_WITHOUT_EXPECT") -internal actual typealias ReentrantLock = java.util.concurrent.locks.ReentrantLock - -internal actual inline fun ReentrantLock.withLock(action: () -> T) = this.withLockJvm(action) - -@Suppress("NOTHING_TO_INLINE") // So that R8 can completely remove ConcurrentKt class -internal actual inline fun identitySet(expectedSize: Int): MutableSet = - Collections.newSetFromMap(IdentityHashMap(expectedSize)) +internal actual fun identitySet(expectedSize: Int): MutableSet = Collections.newSetFromMap(IdentityHashMap(expectedSize)) private val REMOVE_FUTURE_ON_CANCEL: Method? = try { ScheduledThreadPoolExecutor::class.java.getMethod("setRemoveOnCancelPolicy", Boolean::class.java) diff --git a/kotlinx-coroutines-core/jvm/src/internal/ManualMemoryManagement.kt b/kotlinx-coroutines-core/jvm/src/internal/ManualMemoryManagement.kt new file mode 100644 index 0000000000..5eed134b7b --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/internal/ManualMemoryManagement.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") + +package kotlinx.coroutines.internal + +import kotlin.internal.* + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun disposeLockFreeLinkedList(list: () -> LockFreeLinkedListNode?) {} // only needed on Kotlin/Native + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") +internal actual inline fun storeCyclicRef(block: () -> Unit) = block() diff --git a/kotlinx-coroutines-core/jvm/src/internal/Sharing.kt b/kotlinx-coroutines-core/jvm/src/internal/Sharing.kt new file mode 100644 index 0000000000..4e6e22d96c --- /dev/null +++ b/kotlinx-coroutines-core/jvm/src/internal/Sharing.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") + +package kotlinx.coroutines.internal + +import kotlinx.coroutines.* +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* +import kotlin.internal.* + +@Suppress("ACTUAL_WITHOUT_EXPECT") // visibility different +internal actual typealias ShareableRefHolder = Any + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual fun ShareableRefHolder.disposeSharedRef() {} + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual fun T.asShareable(): DisposableHandle where T : DisposableHandle, T : ShareableRefHolder = this + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun CoroutineDispatcher.asShareable(): CoroutineDispatcher = this + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun Continuation.asShareable() : Continuation = this + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun Continuation.asLocal() : Continuation = this + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun Continuation.asLocalOrNull() : Continuation? = this + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual fun Continuation.asLocalOrNullIfNotUsed() : Continuation? = this + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun Continuation.useLocal() : Continuation = this + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun Continuation.shareableInterceptedResumeCancellableWith(result: Result) { + intercepted().resumeCancellableWith(result) +} + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun Continuation.shareableInterceptedResumeWith(result: Result) { + intercepted().resumeWith(result) +} + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun disposeContinuation(cont: () -> Continuation<*>) {} + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Save an entry on call stack +internal actual inline fun CancellableContinuationImpl.shareableResume(delegate: Continuation, useMode: Int) = + resumeImpl(delegate, useMode) + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun (suspend (T) -> R).asShareable(): suspend (T) -> R = this + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Save an entry on call stack +internal actual inline fun isReuseSupportedInPlatform() = true + +@InlineOnly +internal actual inline fun ArrayList.addOrUpdate(element: T, update: (ArrayList) -> Unit) { + add(element) +} + +@InlineOnly +internal actual inline fun ArrayList.addOrUpdate(index: Int, element: T, update: (ArrayList) -> Unit) { + add(index, element) +} + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun Any.weakRef(): Any = this + +@InlineOnly +@Suppress("NOTHING_TO_INLINE") // Should be NOP +internal actual inline fun Any?.unweakRef(): Any? = this diff --git a/kotlinx-coroutines-core/jvm/src/internal/Synchronized.kt b/kotlinx-coroutines-core/jvm/src/internal/Synchronized.kt deleted file mode 100644 index 2b57b26cbd..0000000000 --- a/kotlinx-coroutines-core/jvm/src/internal/Synchronized.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.internal - -import kotlinx.coroutines.* - -/** - * @suppress **This an internal API and should not be used from general code.** - */ -@InternalCoroutinesApi -public actual typealias SynchronizedObject = Any - -/** - * @suppress **This an internal API and should not be used from general code.** - */ -@InternalCoroutinesApi -public actual inline fun synchronized(lock: SynchronizedObject, block: () -> T): T = - kotlin.synchronized(lock, block) diff --git a/kotlinx-coroutines-core/jvm/test/StressUtil.kt b/kotlinx-coroutines-core/jvm/test/StressUtil.kt new file mode 100644 index 0000000000..54ee775d7a --- /dev/null +++ b/kotlinx-coroutines-core/jvm/test/StressUtil.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.random.* + +actual fun randomWait() { + val n = Random.nextInt(1000) + if (n < 500) return // no wait 50% of time + repeat(n) { + BlackHole.sink *= 3 + } + if (n > 900) Thread.yield() +} + +private object BlackHole { + var sink = 1 +} diff --git a/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt b/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt index 13023e3122..eba2f06488 100644 --- a/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt +++ b/kotlinx-coroutines-core/jvm/test/exceptions/Exceptions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.exceptions diff --git a/kotlinx-coroutines-core/native/src/Builders.kt b/kotlinx-coroutines-core/native/src/Builders.kt index 3554dc604e..cd3944bc7c 100644 --- a/kotlinx-coroutines-core/native/src/Builders.kt +++ b/kotlinx-coroutines-core/native/src/Builders.kt @@ -4,9 +4,9 @@ package kotlinx.coroutines -import kotlinx.cinterop.* -import platform.posix.* +import kotlinx.coroutines.internal.* import kotlin.coroutines.* +import kotlin.native.concurrent.* /** * Runs new coroutine and **blocks** current thread _interruptibly_ until its completion. @@ -30,14 +30,14 @@ import kotlin.coroutines.* * @param context context of the coroutine. The default value is an implementation of [EventLoop]. * @param block the coroutine code. */ -public fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T { +public actual fun runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T { val contextInterceptor = context[ContinuationInterceptor] val eventLoop: EventLoop? var newContext: CoroutineContext = context // todo: kludge for data flow analysis error if (contextInterceptor == null) { // create or use private event loop if no dispatcher is specified eventLoop = ThreadLocalEventLoop.eventLoop - newContext = GlobalScope.newCoroutineContext(context + eventLoop) + newContext = GlobalScope.newCoroutineContext(context + eventLoop.asShareable()) } else { // See if context's interceptor is an event loop that we shall use (to support TestContext) // or take an existing thread-local event loop if present to avoid blocking it (but don't create one) @@ -45,36 +45,88 @@ public fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, bl ?: ThreadLocalEventLoop.currentOrNull() newContext = GlobalScope.newCoroutineContext(context) } - val coroutine = BlockingCoroutine(newContext, eventLoop) + val coroutine = BlockingCoroutine(newContext) coroutine.start(CoroutineStart.DEFAULT, coroutine, block) - return coroutine.joinBlocking() + return coroutine.joinBlocking(eventLoop) } private class BlockingCoroutine( - parentContext: CoroutineContext, - private val eventLoop: EventLoop? + parentContext: CoroutineContext ) : AbstractCoroutine(parentContext, true) { override val isScopedCoroutine: Boolean get() = true + private val worker = Worker.current + override fun afterCompletion(state: Any?) { + // wake up blocked worker + if (Worker.current != worker) + worker.execute(TransferMode.SAFE, {}) {} // send an empty task + } + @Suppress("UNCHECKED_CAST") - fun joinBlocking(): T = memScoped { - try { - eventLoop?.incrementUseCount() - val timespec = alloc() - while (true) { - val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE - // note: process next even may loose unpark flag, so check if completed before parking - if (isCompleted) break - timespec.tv_sec = (parkNanos / 1000000000L).convert() // 1e9 ns -> sec - timespec.tv_nsec = (parkNanos % 1000000000L).convert() // % 1e9 - nanosleep(timespec.ptr, null) - } - } finally { // paranoia - eventLoop?.decrementUseCount() - } + fun joinBlocking(eventLoop: EventLoop?): T { + runEventLoop(eventLoop) { isCompleted } // now return result val state = state.unboxState() (state as? CompletedExceptionally)?.let { throw it.cause } - state as T + return state as T + } +} + +internal fun runEventLoop(eventLoop: EventLoop?, isCompleted: () -> Boolean) { + try { + eventLoop?.incrementUseCount() + val thread = currentThread() + while (!isCompleted()) { + val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE + if (isCompleted()) break + thread.parkNanos(parkNanos) + } + } finally { // paranoia + eventLoop?.decrementUseCount() + } +} + +// --------------- Kotlin/Native specialization hooks --------------- + +internal actual fun startCoroutine( + start: CoroutineStart, + receiver: R, + completion: Continuation, + block: suspend R.() -> T +) { + val curThread = currentThread() + val newThread = completion.context[ContinuationInterceptor].thread() + if (newThread != curThread) { + check(start != CoroutineStart.UNDISPATCHED) { + "Cannot start an undispatched coroutine in another thread $newThread from current $curThread" + } + if (start != CoroutineStart.LAZY) { + newThread.execute { + startCoroutineImpl(start, receiver, completion, block) + } + } + return } + startCoroutineImpl(start, receiver, completion, block) } + +private fun ContinuationInterceptor?.thread(): Thread = when (this) { + null -> Dispatchers.Default.thread() + is ThreadBoundInterceptor -> thread + else -> currentThread() // fallback +} + +internal actual fun saveLazyCoroutine( + coroutine: AbstractCoroutine, + receiver: R, + block: suspend R.() -> T +): Any = + block + +@Suppress("NOTHING_TO_INLINE", "UNCHECKED_CAST") // Save an entry on call stack +internal actual fun startLazyCoroutine( + saved: Any, + coroutine: AbstractCoroutine, + receiver: R +) = + startCoroutine(CoroutineStart.DEFAULT, receiver, coroutine, saved as suspend R.() -> T) diff --git a/kotlinx-coroutines-core/native/src/CoroutineContext.kt b/kotlinx-coroutines-core/native/src/CoroutineContext.kt index bcc7f48963..8d0f5d50bc 100644 --- a/kotlinx-coroutines-core/native/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/native/src/CoroutineContext.kt @@ -24,16 +24,13 @@ internal actual object DefaultExecutor : CoroutineDispatcher(), Delay { internal fun loopWasShutDown(): Nothing = error("Cannot execute task because event loop was shut down") -internal actual fun createDefaultDispatcher(): CoroutineDispatcher = - DefaultExecutor - @SharedImmutable internal actual val DefaultDelay: Delay = DefaultExecutor public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext { val combined = coroutineContext + context - return if (combined !== DefaultExecutor && combined[ContinuationInterceptor] == null) - combined + DefaultExecutor else combined + return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null) + combined + Dispatchers.Default else combined } // No debugging facilities on native diff --git a/kotlinx-coroutines-core/native/src/Dispatchers.native.kt b/kotlinx-coroutines-core/native/src/Dispatchers.native.kt new file mode 100644 index 0000000000..685d8d741f --- /dev/null +++ b/kotlinx-coroutines-core/native/src/Dispatchers.native.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.atomicfu.* +import kotlinx.atomicfu.locks.* +import kotlin.coroutines.* + +public actual object Dispatchers { + public actual val Default: CoroutineDispatcher get() = DefaultDispatcher + public actual val Main: MainCoroutineDispatcher = createMainDispatcher(Default) + public actual val Unconfined: CoroutineDispatcher get() = kotlinx.coroutines.Unconfined // Avoid freezing +} + +internal expect fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher + +// Create DefaultDispatcher thread only when explicitly requested +internal object DefaultDispatcher : CoroutineDispatcher(), Delay, ThreadBoundInterceptor { + private val lock = reentrantLock() + private val _delegate = atomic(null) +// private val delegate by lazy { newSingleThreadContext("DefaultDispatcher") } + private val delegate: SingleThreadDispatcher + get() = _delegate.value ?: getOrCreateDefaultDispatcher() + + private fun getOrCreateDefaultDispatcher() = lock.withLock { + _delegate.value ?: newSingleThreadContext("DefaultDispatcher").also { _delegate.value = it } + } + + override val thread: Thread + get() = delegate.thread + override fun dispatch(context: CoroutineContext, block: Runnable) = + delegate.dispatch(context, block) + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) = + (delegate as Delay).scheduleResumeAfterDelay(timeMillis, continuation) + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = + (delegate as Delay).invokeOnTimeout(timeMillis, block) + override fun toString(): String = + delegate.toString() + + // only for tests + internal fun shutdown() { + _delegate.getAndSet(null)?.close() + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/native/src/EventLoop.kt b/kotlinx-coroutines-core/native/src/EventLoop.kt index d6c6525504..432372ca5d 100644 --- a/kotlinx-coroutines-core/native/src/EventLoop.kt +++ b/kotlinx-coroutines-core/native/src/EventLoop.kt @@ -4,17 +4,71 @@ package kotlinx.coroutines +import kotlinx.cinterop.* +import kotlin.coroutines.* +import kotlin.native.concurrent.* import kotlin.system.* internal actual abstract class EventLoopImplPlatform: EventLoop() { - protected actual fun unpark() { /* does nothing */ } + protected actual fun unpark() { + /* + * Does nothing, because we only work with EventLoop in Kotlin/Native from a single thread where + * it was created. All tasks that come from other threads are passed into the owner thread via + * Worker.execute and its queueing mechanics. + */ + } + protected actual fun reschedule(now: Long, delayedTask: EventLoopImplBase.DelayedTask): Unit = loopWasShutDown() } internal class EventLoopImpl: EventLoopImplBase() { + init { ensureNeverFrozen() } + + val shareable = ShareableEventLoop(StableRef.create(this), Worker.current) + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle = scheduleInvokeOnTimeout(timeMillis, block) + + override fun shutdown() { + super.shutdown() + shareable.ref.dispose() + } +} + +internal class ShareableEventLoop( + val ref: StableRef, + private val worker: Worker +) : CoroutineDispatcher(), Delay, ThreadBoundInterceptor { + override val thread: Thread = WorkerThread(worker) + + init { freeze() } + + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { + checkCurrentThread() + ref.get().scheduleResumeAfterDelay(timeMillis, continuation) + } + + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + checkCurrentThread() + return ref.get().invokeOnTimeout(timeMillis, block) + } + + override fun dispatch(context: CoroutineContext, block: Runnable) { + checkCurrentThread() + ref.get().dispatch(context, block) + } + + override fun interceptContinuation(continuation: Continuation): Continuation { + checkCurrentThread() + return ref.get().interceptContinuation(continuation) + } + + @InternalCoroutinesApi + override fun releaseInterceptedContinuation(continuation: Continuation<*>) { + checkCurrentThread() + ref.get().releaseInterceptedContinuation(continuation) + } } internal actual fun createEventLoop(): EventLoop = EventLoopImpl() diff --git a/kotlinx-coroutines-core/native/src/Exceptions.kt b/kotlinx-coroutines-core/native/src/Exceptions.kt index c82199a429..a1bfe92070 100644 --- a/kotlinx-coroutines-core/native/src/Exceptions.kt +++ b/kotlinx-coroutines-core/native/src/Exceptions.kt @@ -4,6 +4,9 @@ package kotlinx.coroutines +import kotlinx.atomicfu.* +import kotlin.native.ref.* + /** * Thrown by cancellable suspending functions if the [Job] of the coroutine is cancelled while it is suspending. * It indicates _normal_ cancellation of a coroutine. @@ -25,8 +28,12 @@ public actual open class CancellationException( internal actual class JobCancellationException public actual constructor( message: String, cause: Throwable?, - internal actual val job: Job + job: Job ) : CancellationException(message, cause) { + private val ref = WeakReference(job) + internal actual val job: Job? + get() = ref.get() + override fun toString(): String = "${super.toString()}; job=$job" override fun equals(other: Any?): Boolean = other === this || @@ -35,8 +42,26 @@ internal actual class JobCancellationException public actual constructor( (message!!.hashCode() * 31 + job.hashCode()) * 31 + (cause?.hashCode() ?: 0) } -@Suppress("NOTHING_TO_INLINE") -internal actual inline fun Throwable.addSuppressedThrowable(other: Throwable) { /* empty */ } +internal actual fun Throwable.addSuppressedThrowable(other: Throwable) { + if (this is SuppressSupportingThrowableImpl) addSuppressed(other) +} + +// "Suppress-supporting throwable" is currently used for tests only +internal open class SuppressSupportingThrowableImpl : Throwable() { + private val _suppressed = atomic?>(null) + + val suppressed: Array + get() = _suppressed.value ?: emptyArray() + + fun addSuppressed(other: Throwable) { + _suppressed.update { current -> + if (current == null) + arrayOf(other) + else + current + other + } + } +} // For use in tests internal actual val RECOVER_STACK_TRACES: Boolean = false diff --git a/kotlinx-coroutines-core/native/src/Thread.native.kt b/kotlinx-coroutines-core/native/src/Thread.native.kt new file mode 100644 index 0000000000..05ab14fc9b --- /dev/null +++ b/kotlinx-coroutines-core/native/src/Thread.native.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.native.concurrent.* + +internal abstract class Thread { + abstract fun execute(block: () -> Unit) + abstract fun parkNanos(timeout: Long) +} + +@ThreadLocal +private val currentThread: Thread = initCurrentThread() + +internal fun currentThread(): Thread = currentThread + +internal expect fun initCurrentThread(): Thread + +internal expect fun Worker.toThread(): Thread + +internal fun Worker.execute(block: () -> Unit) { + block.freeze() + executeAfter(0, block) +} + +internal open class WorkerThread(val worker: Worker = Worker.current) : Thread() { + override fun execute(block: () -> Unit) = worker.execute(block) + + override fun parkNanos(timeout: Long) { + // Note: worker is parked in microseconds + worker.park(timeout / 1000L, process = true) + } + + override fun equals(other: Any?): Boolean = other is WorkerThread && other.worker == worker + override fun hashCode(): Int = worker.hashCode() + override fun toString(): String = worker.name +} + +internal interface ThreadBoundInterceptor { + val thread: Thread +} + +internal fun ThreadBoundInterceptor.checkCurrentThread() { + val current = currentThread() + check(current == thread) { "This dispatcher can be used only from a single thread $thread, but now in $current" } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/native/src/Workers.kt b/kotlinx-coroutines-core/native/src/Workers.kt new file mode 100644 index 0000000000..9f14e321af --- /dev/null +++ b/kotlinx-coroutines-core/native/src/Workers.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.atomicfu.* +import kotlin.coroutines.* +import kotlin.native.concurrent.* + +/** + * Creates a coroutine execution context using a single thread. + */ +@ExperimentalCoroutinesApi +public actual fun newSingleThreadContext(name: String): SingleThreadDispatcher = + WorkerCoroutineDispatcherImpl(name).apply { start() } + +/** + * A coroutine dispatcher that is confined to a single thread. + */ +@ExperimentalCoroutinesApi +@Suppress("ACTUAL_WITHOUT_EXPECT") +public actual abstract class SingleThreadDispatcher : CoroutineDispatcher() { + /** + * A reference to this dispatcher's worker. + */ + @ExperimentalCoroutinesApi + public abstract val worker: Worker + + internal abstract val thread: Thread + + /** + * Closes this coroutine dispatcher and shuts down its thread. + */ + @ExperimentalCoroutinesApi + public actual abstract fun close() +} + +private class WorkerCoroutineDispatcherImpl(name: String) : SingleThreadDispatcher(), ThreadBoundInterceptor, Delay { + override val worker = Worker.start(name = name) + override val thread = WorkerThread(worker) + private val isClosed = atomic(false) + + init { freeze() } + + fun start() { + worker.execute { + workerMain { + runEventLoop(ThreadLocalEventLoop.eventLoop) { isClosed.value } + } + } + } + + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { + checkCurrentThread() + (ThreadLocalEventLoop.eventLoop as Delay).scheduleResumeAfterDelay(timeMillis, continuation) + } + + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + checkCurrentThread() + return (ThreadLocalEventLoop.eventLoop as Delay).invokeOnTimeout(timeMillis, block) + } + + override fun dispatch(context: CoroutineContext, block: Runnable) { + checkCurrentThread() + ThreadLocalEventLoop.eventLoop.dispatch(context, block) + } + + override fun close() { + isClosed.value = true + worker.requestTermination().result // Note: calling "result" blocks + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/native/src/channels/ArrayBufferState.kt b/kotlinx-coroutines-core/native/src/channels/ArrayBufferState.kt new file mode 100644 index 0000000000..9f09e5a446 --- /dev/null +++ b/kotlinx-coroutines-core/native/src/channels/ArrayBufferState.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +import kotlinx.atomicfu.* +import kotlinx.atomicfu.locks.* + +internal actual open class ArrayBufferState actual constructor(initialBufferSize: Int) : SynchronizedObject() { + protected val _buffer = atomic(atomicArrayOfNulls(initialBufferSize)) + protected val _bufferSize = atomic(initialBufferSize) + + actual val bufferSize: Int + get() = _bufferSize.value + + actual fun getBufferAt(index: Int): Any? = + _buffer.value[index].value + + actual fun setBufferAt(index: Int, value: Any?) { + _buffer.value[index].value = value + } + + actual inline fun withLock(block: () -> T): T = + synchronized(this) { + block() + } +} diff --git a/kotlinx-coroutines-core/native/src/channels/ArrayChannelState.kt b/kotlinx-coroutines-core/native/src/channels/ArrayChannelState.kt new file mode 100644 index 0000000000..fac45165a6 --- /dev/null +++ b/kotlinx-coroutines-core/native/src/channels/ArrayChannelState.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +import kotlinx.atomicfu.* +import kotlin.math.* + +internal actual class ArrayChannelState actual constructor(initialBufferSize: Int) : ArrayBufferState(initialBufferSize) { + private val _head = atomic(0) + private val _size = atomic(0) + + actual var head: Int + get() = _head.value + set(value) { _head.value = value } + + actual var size: Int + get() = _size.value + set(value) { _size.value = value } + + actual fun ensureCapacity(currentSize: Int, capacity: Int) { + if (currentSize < bufferSize) return + val newSize = min(bufferSize * 2, capacity) + val newBuffer = atomicArrayOfNulls(newSize) + for (i in 0 until currentSize) { + newBuffer[i].value = _buffer.value[(head + i) % bufferSize].value + } + _buffer.value = newBuffer + _bufferSize.value = newSize + head = 0 + } +} diff --git a/kotlinx-coroutines-core/native/src/channels/ConflatedChannelState.kt b/kotlinx-coroutines-core/native/src/channels/ConflatedChannelState.kt new file mode 100644 index 0000000000..acc144e9e4 --- /dev/null +++ b/kotlinx-coroutines-core/native/src/channels/ConflatedChannelState.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.channels + +import kotlinx.atomicfu.* +import kotlinx.atomicfu.locks.* + +internal actual class ConflatedChannelState : SynchronizedObject() { + private val _value = atomic(EMPTY) + + actual var value: Any? + get() = _value.value + set(value) { _value.value = value } + + actual inline fun withLock(block: () -> T): T = + synchronized(this) { + block() + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/native/src/internal/Concurrent.kt b/kotlinx-coroutines-core/native/src/internal/Concurrent.kt index 486dc8f057..86fb27b038 100644 --- a/kotlinx-coroutines-core/native/src/internal/Concurrent.kt +++ b/kotlinx-coroutines-core/native/src/internal/Concurrent.kt @@ -4,15 +4,6 @@ package kotlinx.coroutines.internal -internal actual typealias ReentrantLock = NoOpLock - -internal actual inline fun ReentrantLock.withLock(action: () -> T) = action() - -internal class NoOpLock { - fun tryLock() = true - fun unlock(): Unit {} -} - internal actual fun subscriberList(): MutableList = CopyOnWriteList() internal actual fun identitySet(expectedSize: Int): MutableSet = HashSet() diff --git a/kotlinx-coroutines-core/native/src/internal/CopyOnWriteList.kt b/kotlinx-coroutines-core/native/src/internal/CopyOnWriteList.kt index b925317b3d..f8e8e7ab78 100644 --- a/kotlinx-coroutines-core/native/src/internal/CopyOnWriteList.kt +++ b/kotlinx-coroutines-core/native/src/internal/CopyOnWriteList.kt @@ -4,101 +4,76 @@ package kotlinx.coroutines.internal +import kotlinx.atomicfu.* + @Suppress("UNCHECKED_CAST") -internal class CopyOnWriteList(private var array: Array = arrayOfNulls(4)) : AbstractMutableList() { +internal class CopyOnWriteList() : AbstractMutableList() { + + private val _array = atomic>(arrayOfNulls(0)) + private var array: Array + get() = _array.value + set(value) { _array.value = value } - private var _size = 0 - override val size: Int get() = _size + override val size: Int + get() = array.size override fun add(element: E): Boolean { - val newSize = if (_size == array.size) array.size * 2 else array.size - val update = array.copyOf(newSize) - update[_size++] = element + val n = size + val update = array.copyOf(n + 1) + update[n] = element array = update return true } override fun add(index: Int, element: E) { rangeCheck(index) - val update = arrayOfNulls(if (array.size == _size) array.size * 2 else array.size) - array.copyInto( - destination = update, - endIndex = index - ) + val n = size + val update = arrayOfNulls(n + 1) + array.copyInto(destination = update, endIndex = index) update[index] = element - array.copyInto( - destination = update, - destinationOffset = index + 1, - startIndex = index, - endIndex = _size + 1 - ) - ++_size + array.copyInto(destination = update, destinationOffset = index + 1, startIndex = index, endIndex = n + 1) array = update } override fun remove(element: E): Boolean { val index = array.indexOf(element as Any) - if (index == -1) { - return false - } - + if (index == -1) return false removeAt(index) return true } override fun removeAt(index: Int): E { rangeCheck(index) - modCount++ - val n = array.size + val n = size val element = array[index] - val update = arrayOfNulls(n) - array.copyInto( - destination = update, - endIndex = index - ) - array.copyInto( - destination = update, - destinationOffset = index, - startIndex = index + 1, - endIndex = n - ) + val update = arrayOfNulls(n - 1) + array.copyInto(destination = update, endIndex = index) + array.copyInto(destination = update, destinationOffset = index, startIndex = index + 1, endIndex = n) array = update - --_size return element as E } - override fun iterator(): MutableIterator = IteratorImpl(array as Array, size) - + override fun iterator(): MutableIterator = IteratorImpl(array as Array) override fun listIterator(): MutableListIterator = throw UnsupportedOperationException("Operation is not supported") - override fun listIterator(index: Int): MutableListIterator = throw UnsupportedOperationException("Operation is not supported") - override fun isEmpty(): Boolean = size == 0 - override fun set(index: Int, element: E): E = throw UnsupportedOperationException("Operation is not supported") - - override fun get(index: Int): E = array[rangeCheck(index)]!! as E - - private class IteratorImpl(private var array: Array, private val size: Int) : MutableIterator { - + override fun get(index: Int): E = array[rangeCheck(index)] as E + + private class IteratorImpl(private val array: Array) : MutableIterator { private var current = 0 - override fun hasNext(): Boolean = current != size + override fun hasNext(): Boolean = current != array.size override fun next(): E { - if (!hasNext()) { - throw NoSuchElementException() - } - - return array[current++]!! + if (!hasNext()) throw NoSuchElementException() + return array[current++] } override fun remove() = throw UnsupportedOperationException("Operation is not supported") } private fun rangeCheck(index: Int) = index.apply { - if (index < 0 || index >= _size) { - throw IndexOutOfBoundsException("index: $index, size: $size") - } + if (index < 0 || index >= size) throw IndexOutOfBoundsException("index: $index, size: $size") } } diff --git a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt b/kotlinx-coroutines-core/native/src/internal/LinkedList.kt deleted file mode 100644 index 9657830e35..0000000000 --- a/kotlinx-coroutines-core/native/src/internal/LinkedList.kt +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ -@file:Suppress("NO_EXPLICIT_RETURN_TYPE_IN_API_MODE", "NO_EXPLICIT_VISIBILITY_IN_API_MODE") - -package kotlinx.coroutines.internal - -private typealias Node = LinkedListNode - -/** @suppress **This is unstable API and it is subject to change.** */ -@Suppress("NO_ACTUAL_CLASS_MEMBER_FOR_EXPECTED_CLASS") // :TODO: Remove when fixed: https://youtrack.jetbrains.com/issue/KT-23703 -public actual typealias LockFreeLinkedListNode = LinkedListNode - -/** @suppress **This is unstable API and it is subject to change.** */ -public actual typealias LockFreeLinkedListHead = LinkedListHead - -/** @suppress **This is unstable API and it is subject to change.** */ -public open class LinkedListNode { - @PublishedApi internal var _next = this - @PublishedApi internal var _prev = this - @PublishedApi internal var _removed: Boolean = false - - public inline val nextNode get() = _next - public inline val prevNode get() = _prev - public inline val isRemoved get() = _removed - - public fun addLast(node: Node) { - val prev = this._prev - node._next = this - node._prev = prev - prev._next = node - this._prev = node - } - - public open fun remove(): Boolean { - if (_removed) return false - val prev = this._prev - val next = this._next - prev._next = next - next._prev = prev - _removed = true - return true - } - - public fun addOneIfEmpty(node: Node): Boolean { - if (_next !== this) return false - addLast(node) - return true - } - - public inline fun addLastIf(node: Node, crossinline condition: () -> Boolean): Boolean { - if (!condition()) return false - addLast(node) - return true - } - - public inline fun addLastIfPrev(node: Node, predicate: (Node) -> Boolean): Boolean { - if (!predicate(_prev)) return false - addLast(node) - return true - } - - public inline fun addLastIfPrevAndIf( - node: Node, - predicate: (Node) -> Boolean, // prev node predicate - crossinline condition: () -> Boolean // atomically checked condition - ): Boolean { - if (!predicate(_prev)) return false - if (!condition()) return false - addLast(node) - return true - } - - public fun helpRemove() {} // no-op without multithreading - - public fun removeFirstOrNull(): Node? { - val next = _next - if (next === this) return null - check(next.remove()) { "Should remove" } - return next - } - - public inline fun removeFirstIfIsInstanceOfOrPeekIf(predicate: (T) -> Boolean): T? { - val next = _next - if (next === this) return null - if (next !is T) return null - if (predicate(next)) return next - check(next.remove()) { "Should remove" } - return next - } -} - -/** @suppress **This is unstable API and it is subject to change.** */ -public actual open class AddLastDesc actual constructor( - actual val queue: Node, - actual val node: T -) : AbstractAtomicDesc() { - override val affectedNode: Node get() = queue._prev - actual override fun finishPrepare(prepareOp: PrepareOp) {} - override fun onComplete() = queue.addLast(node) - actual override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) = Unit -} - -/** @suppress **This is unstable API and it is subject to change.** */ -public actual open class RemoveFirstDesc actual constructor( - actual val queue: LockFreeLinkedListNode -) : AbstractAtomicDesc() { - @Suppress("UNCHECKED_CAST") - actual val result: T get() = affectedNode as T - override val affectedNode: Node = queue.nextNode - actual override fun finishPrepare(prepareOp: PrepareOp) {} - override fun onComplete() { queue.removeFirstOrNull() } - actual override fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) = Unit -} - -/** @suppress **This is unstable API and it is subject to change.** */ -public actual abstract class AbstractAtomicDesc : AtomicDesc() { - protected abstract val affectedNode: Node - actual abstract fun finishPrepare(prepareOp: PrepareOp) - protected abstract fun onComplete() - - actual open fun onPrepare(prepareOp: PrepareOp): Any? { - finishPrepare(prepareOp) - return null - } - - actual final override fun prepare(op: AtomicOp<*>): Any? { - val affected = affectedNode - val failure = failure(affected) - if (failure != null) return failure - @Suppress("UNCHECKED_CAST") - return onPrepare(PrepareOp(affected, this, op)) - } - - actual final override fun complete(op: AtomicOp<*>, failure: Any?) = onComplete() - protected actual open fun failure(affected: LockFreeLinkedListNode): Any? = null // Never fails by default - protected actual open fun retry(affected: LockFreeLinkedListNode, next: Any): Boolean = false // Always succeeds - protected actual abstract fun finishOnSuccess(affected: LockFreeLinkedListNode, next: LockFreeLinkedListNode) -} - -/** @suppress **This is unstable API and it is subject to change.** */ -public actual class PrepareOp( - actual val affected: LockFreeLinkedListNode, - actual val desc: AbstractAtomicDesc, - actual override val atomicOp: AtomicOp<*> -): OpDescriptor() { - override fun perform(affected: Any?): Any? = null - actual fun finishPrepare() {} -} - -/** @suppress **This is unstable API and it is subject to change.** */ -public open class LinkedListHead : LinkedListNode() { - public val isEmpty get() = _next === this - - /** - * Iterates over all elements in this list of a specified type. - */ - public inline fun forEach(block: (T) -> Unit) { - var cur: Node = _next - while (cur != this) { - if (cur is T) block(cur) - cur = cur._next - } - } - - // just a defensive programming -- makes sure that list head sentinel is never removed - public final override fun remove(): Boolean = throw UnsupportedOperationException() -} diff --git a/kotlinx-coroutines-core/native/src/internal/ManualMemoryManagement.kt b/kotlinx-coroutines-core/native/src/internal/ManualMemoryManagement.kt new file mode 100644 index 0000000000..454925b405 --- /dev/null +++ b/kotlinx-coroutines-core/native/src/internal/ManualMemoryManagement.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +@Suppress("NOTHING_TO_INLINE") +internal actual inline fun disposeLockFreeLinkedList(list: () -> LockFreeLinkedListNode?) { + // only needed on Kotlin/Native + val head = list() ?: return + var cur = head + do { + val next = cur.nextNode // returns cur when already unlinked last node + val last = next === head || next === cur + cur.unlinkRefs(last) + cur = next + } while (!last) +} + +@Suppress("NOTHING_TO_INLINE") +internal actual inline fun storeCyclicRef(block: () -> Unit) {} // nop on native diff --git a/kotlinx-coroutines-core/native/src/internal/Sharing.kt b/kotlinx-coroutines-core/native/src/internal/Sharing.kt new file mode 100644 index 0000000000..ea588f6b6d --- /dev/null +++ b/kotlinx-coroutines-core/native/src/internal/Sharing.kt @@ -0,0 +1,222 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlinx.atomicfu.* +import kotlinx.coroutines.* +import kotlin.coroutines.* +import kotlin.coroutines.intrinsics.* +import kotlin.native.concurrent.* +import kotlin.native.ref.* + +internal actual open class ShareableRefHolder { + internal var shareable: ShareableObject<*>? = null // cached result of asShareable call +} + +internal actual fun ShareableRefHolder.disposeSharedRef() { + shareable?.disposeRef() +} + +internal actual fun T.asShareable(): DisposableHandle where T : DisposableHandle, T : ShareableRefHolder { + shareable?.let { return it as DisposableHandle } + return ShareableDisposableHandle(this).also { shareable = it } +} + +internal actual fun CoroutineDispatcher.asShareable(): CoroutineDispatcher = when (this) { + is EventLoopImpl -> shareable + else -> this +} + +internal actual fun Continuation.asShareable() : Continuation = when (this) { + is ShareableContinuation -> this + else -> ShareableContinuation(this) +} + +internal actual fun Continuation.asLocal() : Continuation = when (this) { + is ShareableContinuation -> localRef() + else -> this +} + +internal actual fun Continuation.asLocalOrNull() : Continuation? = when (this) { + is ShareableContinuation -> localRefOrNull() + else -> this +} + +internal actual fun Continuation.asLocalOrNullIfNotUsed() : Continuation? = when (this) { + is ShareableContinuation -> localRefOrNullIfNotUsed() + else -> this +} + +internal actual fun Continuation.useLocal() : Continuation = when (this) { + is ShareableContinuation -> useRef() + else -> this +} + +internal actual fun Continuation.shareableInterceptedResumeCancellableWith(result: Result) { + this as ShareableContinuation // must have been shared + val thread = ownerThreadOrNull ?: wasUsed() + if (currentThread() == thread) { + useRef().intercepted().resumeCancellableWith(result) + } else { + thread.execute { + useRef().intercepted().resumeCancellableWith(result) + } + } +} + +internal actual fun Continuation.shareableInterceptedResumeWith(result: Result) { + this as ShareableContinuation // must have been shared + val thread = ownerThreadOrNull ?: wasUsed() + if (currentThread() == thread) { + useRef().intercepted().resumeWith(result) + } else { + thread.execute { + useRef().intercepted().resumeWith(result) + } + } +} + +internal actual fun (suspend (T) -> R).asShareable(): suspend (T) -> R = + ShareableBlock(this) + +@PublishedApi +internal actual inline fun disposeContinuation(cont: () -> Continuation<*>) { + (cont() as ShareableContinuation<*>).disposeRef() +} + +internal actual fun CancellableContinuationImpl.shareableResume(delegate: Continuation, useMode: Int) { + if (delegate is ShareableContinuation) { + val thread = delegate.ownerThreadOrNull ?: delegate.wasUsed() + if (currentThread() == thread) { + resumeImpl(delegate.useRef(), useMode) + } else { + thread.execute { + resumeImpl(delegate.useRef(), useMode) + } + } + return + } + resumeImpl(delegate, useMode) +} + +internal actual fun isReuseSupportedInPlatform() = false + +internal actual inline fun ArrayList.addOrUpdate(element: T, update: (ArrayList) -> Unit) { + if (isFrozen) { + val list = ArrayList(size + 1) + list.addAll(this) + list.add(element) + update(list) + } else { + add(element) + } +} + +internal actual inline fun ArrayList.addOrUpdate(index: Int, element: T, update: (ArrayList) -> Unit) { + if (isFrozen) { + val list = ArrayList(size + 1) + list.addAll(this) + list.add(index, element) + update(list) + } else { + add(index, element) + } +} + +@Suppress("NOTHING_TO_INLINE") +internal actual inline fun Any.weakRef(): Any = WeakReference(this) + +internal actual fun Any?.unweakRef(): Any? = (this as WeakReference<*>?)?.get() + +internal open class ShareableObject(obj: T) { + private val _ref = atomic?>(WorkerBoundReference(obj)) + + val ownerThreadOrNull: Thread? + get() = _ref.value?.worker?.toThread() + + fun localRef(): T { + val ref = _ref.value ?: wasUsed() + return ref.value + } + + fun localRefOrNull(): T? { + val ref = _ref.value ?: wasUsed() + if (Worker.current != ref.worker) return null + return ref.value + } + + fun localRefOrNullIfNotUsed(): T? { + val ref = _ref.value ?: return null + if (Worker.current != ref.worker) return null + return ref.value + } + + fun useRef(): T { + val ref = _ref.getAndSet(null) ?: wasUsed() + return ref.value + } + + fun disposeRef(): T? { + val ref = _ref.getAndSet(null) ?: return null + return ref.value + } + + fun wasUsed(): Nothing { + error("Ref $classSimpleName@$hexAddress was already used") + } + + override fun toString(): String { + val ref = _ref.value ?: return "Shareable[used]" + return "Shareable[${if (Worker.current == ref.worker) _ref.value.toString() else "wrong worker"}]" + } +} + +@PublishedApi +internal class ShareableContinuation( + cont: Continuation +) : ShareableObject>(cont), Continuation { + override val context: CoroutineContext = cont.context + + override fun resumeWith(result: Result) { + val thread = ownerThreadOrNull ?: wasUsed() + if (currentThread() == thread) { + useRef().resumeWith(result) + } else { + thread.execute { + useRef().resumeWith(result) + } + } + } +} + +private class ShareableDisposableHandle( + handle: DisposableHandle +) : ShareableObject(handle), DisposableHandle { + override fun dispose() { + val thread = ownerThreadOrNull ?: return + if (currentThread() == thread) { + disposeRef()?.dispose() + } else { + thread.execute { + disposeRef()?.dispose() + } + } + } +} + +private typealias Block1 = suspend (T) -> R +private typealias Fun2 = Function2, Any?> + +// todo: SuspendFunction impl is a hack to workaround the absence of proper suspend fun implementation ability +@Suppress("SUPERTYPE_IS_SUSPEND_FUNCTION_TYPE", "INCONSISTENT_TYPE_PARAMETER_VALUES") +private class ShareableBlock( + block: Block1 +) : ShareableObject>(block), Block1, SuspendFunction, Fun2 { + override suspend fun invoke(param: T): R = useRef().invoke(param) + + @Suppress("UNCHECKED_CAST") + override fun invoke(param: T, cont: Continuation): Any? = + (useRef() as Fun2).invoke(param, cont) +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/native/src/internal/Synchronized.kt b/kotlinx-coroutines-core/native/src/internal/Synchronized.kt deleted file mode 100644 index 0911dbe115..0000000000 --- a/kotlinx-coroutines-core/native/src/internal/Synchronized.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.internal - -import kotlinx.coroutines.* - -/** - * @suppress **This an internal API and should not be used from general code.** - */ -@InternalCoroutinesApi -public actual typealias SynchronizedObject = Any - -/** - * @suppress **This an internal API and should not be used from general code.** - */ -@InternalCoroutinesApi -public actual inline fun synchronized(lock: SynchronizedObject, block: () -> T): T = - block() diff --git a/kotlinx-coroutines-core/native/test/DefaultDispatcherTest.kt b/kotlinx-coroutines-core/native/test/DefaultDispatcherTest.kt new file mode 100644 index 0000000000..d19e38c5a7 --- /dev/null +++ b/kotlinx-coroutines-core/native/test/DefaultDispatcherTest.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.test.* + +class DefaultDispatcherTest : TestBase() { + private val testThread = currentThread() + + @Test + fun testDefaultDispatcher() = runTest { + expect(1) + withContext(Dispatchers.Default) { + assertTrue(currentThread() != testThread) + expect(2) + } + assertEquals(testThread, currentThread()) + finish(3) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/native/test/EventLoopTest.kt b/kotlinx-coroutines-core/native/test/EventLoopTest.kt new file mode 100644 index 0000000000..f30bf1a902 --- /dev/null +++ b/kotlinx-coroutines-core/native/test/EventLoopTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.coroutines.* +import kotlin.test.* + +/** + * Ensure that there are no leaks because of various delay usage. + */ +class EventLoopTest : TestBase() { + @Test + fun testDelayWait() = runTest { + expect(1) + delay(1) + finish(2) + } + + @Test + fun testDelayCancel() = runTest { + expect(1) + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + delay(100) + expectUnreached() + } + expect(3) + job.cancel() + finish(4) + } + + @Test + fun testCancellableContinuationResumeUndispatchedCancelled() = runTest { + expect(1) + var cont: CancellableContinuation? = null + val job = launch(start = CoroutineStart.UNDISPATCHED) { + expect(2) + assertFailsWith { + suspendCancellableCoroutine { cont = it } + } + expect(5) + } + expect(3) + with(cont!!) { + cancel() + // already cancelled, so nothing should happen on resumeUndispatched + (coroutineContext[ContinuationInterceptor] as CoroutineDispatcher).resumeUndispatched(Unit) + } + expect(4) + yield() + finish(6) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/native/test/FreezingTest.kt b/kotlinx-coroutines-core/native/test/FreezingTest.kt new file mode 100644 index 0000000000..cd3cc6a335 --- /dev/null +++ b/kotlinx-coroutines-core/native/test/FreezingTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.native.concurrent.* +import kotlin.test.* + +class FreezingTest : TestBase() { + @Test + fun testFreezeWithContextOther() = runTest { + // create a mutable object referenced by this lambda + val mutable = mutableListOf() + // run a child coroutine in another thread + val result = withContext(Dispatchers.Default) { "OK" } + assertEquals("OK", result) + // ensure that objects referenced by this lambda were not frozen + assertFalse(mutable.isFrozen) + mutable.add(42) // just to be 100% sure + } + + @Test + fun testNoFreezeLaunchSame() = runTest { + // create a mutable object referenced by this lambda + val mutable1 = mutableListOf() + // this one will get captured into the other thread's lambda + val mutable2 = mutableListOf() + val job = launch { // launch into the same context --> should not freeze + assertEquals(mutable1.isFrozen, false) + assertEquals(mutable2.isFrozen, false) + val result = withContext(Dispatchers.Default) { + assertEquals(mutable2.isFrozen, true) // was frozen now + "OK" + } + assertEquals("OK", result) + assertEquals(mutable1.isFrozen, false) + } + job.join() + assertEquals(mutable1.isFrozen, false) + mutable1.add(42) // just to be 100% sure + } + + @Test + fun testFrozenParentJob() { + val parent = Job() + parent.freeze() + val job = Job(parent) + assertTrue(job.isActive) + parent.cancel() + assertTrue(job.isCancelled) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/native/test/ParkStressTest.kt b/kotlinx-coroutines-core/native/test/ParkStressTest.kt new file mode 100644 index 0000000000..c9fbe1740b --- /dev/null +++ b/kotlinx-coroutines-core/native/test/ParkStressTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.native.concurrent.* +import kotlin.test.* + +private const val timeoutMicroseconds = Long.MAX_VALUE / 1000L // too long. +private const val nTasks = 10_000 // repeat test + +/** + * This stress test ensures that Worker.park correctly wakes up. + */ +class ParkStressTest { + @Test + fun testPark() { + val worker = Worker.start() + worker.execute(TransferMode.SAFE, {}) { + // process nTasks + while (TaskCounter.counter < nTasks) { + randomWait() + val ok = Worker.current.park(timeoutMicroseconds, process = true) + assertTrue(ok, "Must have processed a task") + } + assertEquals(nTasks, TaskCounter.counter) + } + // submit nTasks + repeat(nTasks) { index -> + randomWait() + val operation: () -> Unit = { + TaskCounter.counter++ + } + operation.freeze() + worker.executeAfter(0, operation) + } + // shutdown worker + worker.requestTermination().result // block until termination + } +} + +@ThreadLocal +private object TaskCounter { + var counter = 0 +} + diff --git a/kotlinx-coroutines-core/native/test/StressUtil.kt b/kotlinx-coroutines-core/native/test/StressUtil.kt new file mode 100644 index 0000000000..c89c79e936 --- /dev/null +++ b/kotlinx-coroutines-core/native/test/StressUtil.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import platform.posix.* +import kotlin.native.concurrent.* +import kotlin.random.* + +actual fun randomWait() { + val n = Random.nextInt(1000) + if (n < 500) return // no wait 50% of time + repeat(n) { + BlackHole.sink *= 3 + } + if (n > 900) sched_yield() +} + +@ThreadLocal +private object BlackHole { + var sink = 1 +} diff --git a/kotlinx-coroutines-core/native/test/TestBase.kt b/kotlinx-coroutines-core/native/test/TestBase.kt index 890f029ca2..edcc8b9052 100644 --- a/kotlinx-coroutines-core/native/test/TestBase.kt +++ b/kotlinx-coroutines-core/native/test/TestBase.kt @@ -1,16 +1,18 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines +import kotlinx.atomicfu.* + public actual val isStressTest: Boolean = false public actual val stressTestMultiplier: Int = 1 public actual open class TestBase actual constructor() { - private var actionIndex = 0 - private var finished = false - private var error: Throwable? = null + private val actionIndex = atomic(0) + private val finished = atomic(false) + private val error = atomic(null) /** * Throws [IllegalStateException] like `error` in stdlib, but also ensures that the test will not @@ -19,20 +21,21 @@ public actual open class TestBase actual constructor() { @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") public actual fun error(message: Any, cause: Throwable? = null): Nothing { val exception = IllegalStateException(message.toString(), cause) - if (error == null) error = exception + error.compareAndSet(null, exception) throw exception } private fun printError(message: String, cause: Throwable) { - if (error == null) error = cause - println("$message: $cause") + error.compareAndSet(null, cause) + println(message) + cause.printStackTrace() } /** * Asserts that this invocation is `index`-th in the execution sequence (counting from one). */ public actual fun expect(index: Int) { - val wasIndex = ++actionIndex + val wasIndex = actionIndex.incrementAndGet() check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" } } @@ -48,21 +51,21 @@ public actual open class TestBase actual constructor() { */ public actual fun finish(index: Int) { expect(index) - check(!finished) { "Should call 'finish(...)' at most once" } - finished = true + val old = finished.getAndSet(true) + check(!old) { "Should call 'finish(...)' at most once" } } /** * Asserts that [finish] was invoked */ public actual fun ensureFinished() { - require(finished) { "finish(...) should be caller prior to this check" } + require(finished.value) { "finish(...) should be caller prior to this check" } } public actual fun reset() { - check(actionIndex == 0 || finished) { "Expecting that 'finish(...)' was invoked, but it was not" } - actionIndex = 0 - finished = false + check(actionIndex.value == 0 || finished.value) { "Expecting that 'finish(...)' was invoked, but it was not" } + actionIndex.value = 0 + finished.value = false } @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") @@ -71,30 +74,30 @@ public actual open class TestBase actual constructor() { unhandled: List<(Throwable) -> Boolean> = emptyList(), block: suspend CoroutineScope.() -> Unit ) { - var exCount = 0 - var ex: Throwable? = null + val exCount = atomic(0) + val ex = atomic(null) try { - runBlocking(block = block, context = CoroutineExceptionHandler { context, e -> + runBlocking(block = block, context = CoroutineExceptionHandler { _, e -> if (e is CancellationException) return@CoroutineExceptionHandler // are ignored - exCount++ + val result = exCount.incrementAndGet() when { - exCount > unhandled.size -> - printError("Too many unhandled exceptions $exCount, expected ${unhandled.size}, got: $e", e) - !unhandled[exCount - 1](e) -> + result > unhandled.size -> + printError("Too many unhandled exceptions $result, expected ${unhandled.size}, got: $e", e) + !unhandled[result - 1](e) -> printError("Unhandled exception was unexpected: $e", e) } }) } catch (e: Throwable) { - ex = e + ex.value = e if (expected != null) { if (!expected(e)) error("Unexpected exception: $e", e) } else throw e } finally { - if (ex == null && expected != null) error("Exception was expected but none produced") + if (ex.value == null && expected != null) error("Exception was expected but none produced") } - if (exCount < unhandled.size) + if (exCount.value < unhandled.size) error("Too few unhandled exceptions $exCount, expected ${unhandled.size}") } } diff --git a/kotlinx-coroutines-core/native/test/WorkerDispatcherTest.kt b/kotlinx-coroutines-core/native/test/WorkerDispatcherTest.kt new file mode 100644 index 0000000000..f6add43adc --- /dev/null +++ b/kotlinx-coroutines-core/native/test/WorkerDispatcherTest.kt @@ -0,0 +1,317 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.sync.* +import kotlin.native.concurrent.* +import kotlin.test.* + +class WorkerDispatcherTest : TestBase() { + private val dispatcher = newSingleThreadContext("WorkerCoroutineDispatcherTest") + private val mainThread = currentThread() + + @AfterTest + fun tearDown() { + dispatcher.close() + } + + @Test + fun testWithContext() = runTest { + val atomic = AtomicInt(0) // can be captured & shared + expect(1) + val result = withContext(dispatcher) { + expect(2) + assertEquals(dispatcher.thread, currentThread()) + atomic.value = 42 + "OK" + } + assertEquals(mainThread, currentThread()) + assertEquals("OK", result) + assertEquals(42, atomic.value) + finish(3) + } + + @Test + fun testLaunchJoin() = runTest { + val atomic = AtomicInt(0) // can be captured & shared + expect(1) + val job = launch(dispatcher) { + assertEquals(dispatcher.thread, currentThread()) + atomic.value = 42 + } + job.join() + assertEquals(mainThread, currentThread()) + assertEquals(42, atomic.value) + finish(2) + } + + @Test + fun testLaunchLazyJoin() = runTest { + expect(1) + val job = launch(dispatcher, start = CoroutineStart.LAZY) { + expect(3) + assertEquals(dispatcher.thread, currentThread()) + } + expect(2) + job.join() // lazy start here + finish(4) + } + + @Test + fun testAsyncAwait() = runTest { + val atomic = AtomicInt(0) // can be captured & shared + expect(1) + val deferred = async(dispatcher) { + assertEquals(dispatcher.thread, currentThread()) + atomic.value = 42 + "OK" + } + val result = deferred.await() + assertEquals(mainThread, currentThread()) + assertEquals("OK", result) + assertEquals(42, atomic.value) + finish(2) + } + + @Test + fun testAsyncLazyAwait() = runTest { + expect(1) + val deferred = async(dispatcher, start = CoroutineStart.LAZY) { + expect(3) + assertEquals(dispatcher.thread, currentThread()) + "OK" + } + expect(2) + val result = deferred.await() // lazy start here + assertEquals("OK", result) + finish(4) + } + + @Test + fun testProduceConsumeRendezvous() = checkProduceConsume(Channel.RENDEZVOUS) + + @Test + fun testProduceConsumeUnlimited() = checkProduceConsume(Channel.UNLIMITED) + + @Test + fun testProduceConsumeBuffered() = checkProduceConsume(10) + + private fun checkProduceConsume(capacity: Int) { + runTest { + val atomic = AtomicInt(0) // can be captured & shared + expect(1) + val channel = produce(dispatcher, capacity) { + assertEquals(dispatcher.thread, currentThread()) + atomic.value = 42 + expect(2) + send(Data("A")) + send(Data("B")) + } + val result1 = channel.receive() + expect(3) + assertEquals(mainThread, currentThread()) + assertEquals("A", result1.s) + assertTrue(result1.isFrozen) + assertEquals(42, atomic.value) + val result2 = channel.receive() + assertEquals("B", result2.s) + assertEquals(null, channel.receiveOrNull()) // must try to receive the last one to dispose memory + finish(4) + } + } + + @Test + fun testChannelIterator() = runTest { + expect(1) + val channel = RendezvousChannel() + launch(dispatcher) { + channel.send(1) + channel.send(2) + channel.close() + } + var expected = 1 + for (x in channel) { + assertEquals(expected++, x) + } + finish(2) + } + + @Test + fun testArrayBroadcast() = runTest { + expect(1) + val broadcast = BroadcastChannel(10) + val sub = broadcast.openSubscription() + launch(dispatcher) { + assertEquals(dispatcher.thread, currentThread()) + expect(2) + broadcast.send(Data("A")) + broadcast.send(Data("B")) + } + val result1 = sub.receive() + expect(3) + assertEquals(mainThread, currentThread()) + assertEquals("A", result1.s) + assertTrue(result1.isFrozen) + val result2 = sub.receive() + assertEquals("B", result2.s) + sub.cancel() + broadcast.close() // dispose memory + finish(4) + } + + @Test + fun testConflatedBroadcast() = runTest { + expect(1) + val latch = Channel() + val broadcast = ConflatedBroadcastChannel() + val sub = broadcast.openSubscription() + launch(dispatcher) { + assertEquals(dispatcher.thread, currentThread()) + expect(2) + broadcast.send(Data("A")) + latch.receive() + expect(4) + broadcast.send(Data("B")) + } + val result1 = sub.receive() + expect(3) + assertEquals(mainThread, currentThread()) + assertEquals("A", result1.s) + assertTrue(result1.isFrozen) + latch.send(Unit) + val result2 = sub.receive() + assertEquals("B", result2.s) + sub.cancel() + broadcast.close() // dispose memory + latch.close() // dispose memory + finish(5) + } + + @Test + fun testFlowOn() = runTest { + expect(1) + val flow = flow { + expect(3) + assertEquals(dispatcher.thread, currentThread()) + emit(Data("A")) + emit(Data("B")) + }.flowOn(dispatcher) + expect(2) + val result = flow.toList() + assertEquals(listOf(Data("A"), Data("B")), result) + assertTrue(result.all { it.isFrozen }) + finish(4) + } + + @Test + fun testWithContextDelay() = runTest { + expect(1) + withContext(dispatcher) { + expect(2) + delay(10) + assertEquals(dispatcher.thread, currentThread()) + expect(3) + } + finish(4) + } + + @Test + fun testWithTimeoutAroundWithContextNoTimeout() = runTest { + expect(1) + withTimeout(1000) { + withContext(dispatcher) { + expect(2) + } + } + finish(3) + } + + @Test + fun testWithTimeoutAroundWithContextTimedOut() = runTest { + expect(1) + assertFailsWith { + withTimeout(100) { + withContext(dispatcher) { + expect(2) + delay(1000) + } + } + } + finish(3) + } + + @Test + fun testMutexStress() = runTest { + expect(1) + val mutex = Mutex() + val atomic = AtomicInt(0) + val n = 100 + val k = 239 // mutliplier + val job = launch(dispatcher) { + repeat(n) { + mutex.withLock { + atomic.value = atomic.value + 1 // unsafe mutation but under mutex + } + } + } + // concurrently mutate + repeat(n) { + mutex.withLock { + atomic.value = atomic.value + k + } + } + // join job + job.join() + assertEquals((k + 1) * n, atomic.value) + finish(2) + } + + @Test + fun testSemaphoreStress() = runTest { + expect(1) + val semaphore = Semaphore(1) + val atomic = AtomicInt(0) + val n = 100 + val k = 239 // mutliplier + val job = launch(dispatcher) { + repeat(n) { + semaphore.withPermit { + atomic.value = atomic.value + 1 // unsafe mutation but under mutex + } + } + } + // concurrently mutate + repeat(n) { + semaphore.withPermit { + atomic.value = atomic.value + k + } + } + // join job + job.join() + assertEquals((k + 1) * n, atomic.value) + finish(2) + } + + @Test + fun testBroadcastAsFlow() = runTest { + expect(1) + withContext(dispatcher) { + expect(2) + broadcast { + expect(3) + send("OK") + }.asFlow().collect { + expect(4) + assertEquals("OK", it) + } + expect(5) + } + finish(6) + } + + private data class Data(val s: String) +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/native/test/WorkerTest.kt b/kotlinx-coroutines-core/native/test/WorkerTest.kt index 84acedac94..f0f845a258 100644 --- a/kotlinx-coroutines-core/native/test/WorkerTest.kt +++ b/kotlinx-coroutines-core/native/test/WorkerTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines @@ -9,10 +9,15 @@ import kotlin.native.concurrent.* import kotlin.test.* class WorkerTest : TestBase() { + val worker = Worker.start() + + @AfterTest + fun tearDown() { + worker.requestTermination().result + } @Test fun testLaunchInWorker() { - val worker = Worker.start() worker.execute(TransferMode.SAFE, { }) { runBlocking { launch { }.join() @@ -23,7 +28,6 @@ class WorkerTest : TestBase() { @Test fun testLaunchInWorkerTroughGlobalScope() { - val worker = Worker.start() worker.execute(TransferMode.SAFE, { }) { runBlocking { CoroutineScope(EmptyCoroutineContext).launch { diff --git a/kotlinx-coroutines-core/native/test/internal/LinkedListTest.kt b/kotlinx-coroutines-core/native/test/internal/LinkedListTest.kt deleted file mode 100644 index 6c1fddfc5f..0000000000 --- a/kotlinx-coroutines-core/native/test/internal/LinkedListTest.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. - */ - -package kotlinx.coroutines.internal - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class LinkedListTest { - data class IntNode(val i: Int) : LinkedListNode() - - @Test - fun testSimpleAddLastRemove() { - val list = LinkedListHead() - assertContents(list) - val n1 = IntNode(1).apply { list.addLast(this) } - assertContents(list, 1) - val n2 = IntNode(2).apply { list.addLast(this) } - assertContents(list, 1, 2) - val n3 = IntNode(3).apply { list.addLast(this) } - assertContents(list, 1, 2, 3) - val n4 = IntNode(4).apply { list.addLast(this) } - assertContents(list, 1, 2, 3, 4) - assertTrue(n1.remove()) - assertContents(list, 2, 3, 4) - assertTrue(n3.remove()) - assertContents(list, 2, 4) - assertTrue(n4.remove()) - assertContents(list, 2) - assertTrue(n2.remove()) - assertFalse(n2.remove()) - assertContents(list) - } - - private fun assertContents(list: LinkedListHead, vararg expected: Int) { - val n = expected.size - val actual = IntArray(n) - var index = 0 - list.forEach { actual[index++] = it.i } - assertEquals(n, index) - for (i in 0 until n) assertEquals(expected[i], actual[i], "item i") - assertEquals(expected.isEmpty(), list.isEmpty) - } -} diff --git a/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt b/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt new file mode 100644 index 0000000000..dc4f7ed238 --- /dev/null +++ b/kotlinx-coroutines-core/nativeDarwin/src/Dispatchers.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.cinterop.* +import platform.CoreFoundation.* +import platform.darwin.* +import kotlin.coroutines.* +import kotlin.native.concurrent.* +import kotlin.native.internal.NativePtr + +internal fun isMainThread(): Boolean = CFRunLoopGetCurrent() == CFRunLoopGetMain() + +internal actual fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher = + DarwinMainDispatcher(false) + +@Suppress("EXPERIMENTAL_UNSIGNED_LITERALS") +private class DarwinMainDispatcher( + private val invokeImmediately: Boolean +) : MainCoroutineDispatcher(), Delay, ThreadBoundInterceptor { + override val thread + get() = mainThread + + override val immediate: MainCoroutineDispatcher = + if (invokeImmediately) this else DarwinMainDispatcher(true) + + init { freeze() } + + override fun isDispatchNeeded(context: CoroutineContext): Boolean = !invokeImmediately || isMainThread() + + override fun dispatch(context: CoroutineContext, block: Runnable) { + dispatch_async(dispatch_get_main_queue()) { + block.run() + } + } + + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { + val timer = Timer() + val timerBlock: TimerBlock = { + timer.dispose() + continuation.resume(Unit) + } + timer.start(timeMillis, timerBlock) + continuation.disposeOnCancellation(timer) + } + + override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle { + val timer = Timer() + val timerBlock: TimerBlock = { + timer.dispose() + block.run() + } + timer.start(timeMillis, timerBlock) + return timer + } + + override fun toString(): String = + "MainDispatcher${ if(invokeImmediately) "[immediate]" else "" }" +} + +internal typealias TimerBlock = (CFRunLoopTimerRef?) -> Unit + +@SharedImmutable +private val TIMER_NEW = NativePtr.NULL + +@SharedImmutable +private val TIMER_DISPOSED = NativePtr.NULL.plus(1) + +private class Timer : DisposableHandle { + private val ref = AtomicNativePtr(TIMER_NEW) + + init { freeze() } + + fun start(timeMillis: Long, timerBlock: TimerBlock) { + val fireDate = CFAbsoluteTimeGetCurrent() + timeMillis / 1000.0 + @Suppress("EXPERIMENTAL_UNSIGNED_LITERALS") + val timer = CFRunLoopTimerCreateWithHandler(null, fireDate, 0.0, 0u, 0, timerBlock) + CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes) + if (!ref.compareAndSet(TIMER_NEW, timer.rawValue)) { + // dispose was already called concurrently + release(timer) + } + } + + override fun dispose() { + while (true) { + val ptr = ref.value + if (ptr == TIMER_DISPOSED) return + if (ref.compareAndSet(ptr, TIMER_DISPOSED)) { + if (ptr != TIMER_NEW) release(interpretCPointer(ptr)) + return + } + } + } + + private fun release(timer: CFRunLoopTimerRef?) { + CFRunLoopRemoveTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes) + CFRelease(timer) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/nativeDarwin/src/Thread.kt b/kotlinx-coroutines-core/nativeDarwin/src/Thread.kt new file mode 100644 index 0000000000..502507f410 --- /dev/null +++ b/kotlinx-coroutines-core/nativeDarwin/src/Thread.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.atomicfu.* +import kotlinx.cinterop.* +import platform.CoreFoundation.* +import platform.darwin.* +import kotlin.native.concurrent.* + +/** + * Initializes the main thread. Must be called from the main thread if the application's interaction + * with Kotlin runtime and coroutines API otherwise starts from background threads. + */ +@ExperimentalCoroutinesApi +public fun initMainThread() { + getOrCreateMainThread() +} + +internal actual fun initCurrentThread(): Thread = + if (isMainThread()) mainThread else WorkerThread() + +internal actual fun Worker.toThread(): Thread = + if (this == mainThread.worker) mainThread else WorkerThread(this) + +@SharedImmutable +private val _mainThread = AtomicReference(null) + +internal val mainThread: MainThread get() = _mainThread.value ?: getOrCreateMainThread() + +private fun getOrCreateMainThread(): MainThread { + require(isMainThread()) { + "Coroutines must be initialized from the main thread: call 'initMainThread' from the main thread first" + } + _mainThread.value?.let { return it } + return MainThread().also { _mainThread.value = it } +} + +internal class MainThread : WorkerThread() { + private val posted = atomic(false) + + private val processQueueBlock: dispatch_block_t = { + posted.value = false // next execute will post a fresh task + while (worker.processQueue()) { /* process all */ } + } + + init { freeze() } + + override fun execute(block: () -> Unit) { + super.execute(block) + // post to main queue if needed + if (posted.compareAndSet(false, true)) { + dispatch_async(dispatch_get_main_queue(), processQueueBlock) + } + } + + fun shutdown() { + // Cleanup posted processQueueBlock + execute { + CFRunLoopStop(CFRunLoopGetCurrent()) + } + CFRunLoopRun() + assert(!posted.value) // nothing else should have been posted + } + + override fun toString(): String = "MainThread" +} diff --git a/kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt b/kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt index 78ed765967..6489571b9a 100644 --- a/kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt +++ b/kotlinx-coroutines-core/nativeDarwin/test/Launcher.kt @@ -24,5 +24,7 @@ fun mainBackground(args: Array) { fun mainNoExit(args: Array) { workerMain { // autoreleasepool to make sure interop objects are properly freed testLauncherEntryPoint(args) + mainThread.shutdown() + DefaultDispatcher.shutdown() } } \ No newline at end of file diff --git a/kotlinx-coroutines-core/nativeDarwin/test/MainDispatcherTest.kt b/kotlinx-coroutines-core/nativeDarwin/test/MainDispatcherTest.kt new file mode 100644 index 0000000000..6a5a186f4e --- /dev/null +++ b/kotlinx-coroutines-core/nativeDarwin/test/MainDispatcherTest.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.test.* + +class MainDispatcherTest : TestBase() { + private val testThread = currentThread() + + @Test + fun testWithContext() { + if (testThread == mainThread) return // skip if already on the main thread + runTest { + expect(1) + withContext(Dispatchers.Main) { + assertEquals(mainThread, currentThread()) + expect(2) + } + assertEquals(testThread, currentThread()) + finish(3) + } + } + + @Test + fun testWithContextDelay() { + if (testThread == mainThread) return // skip if already on the main thread + runTest { + expect(1) + withContext(Dispatchers.Main) { + assertEquals(mainThread, currentThread()) + expect(2) + delay(100) + assertEquals(mainThread, currentThread()) + expect(3) + } + assertEquals(testThread, currentThread()) + finish(4) + } + } + + @Test + fun testWithTimeoutContextDelayNoTimeout() { + if (testThread == mainThread) return // skip if already on the main thread + runTest { + expect(1) + withTimeout(1000) { + withContext(Dispatchers.Main) { + assertEquals(mainThread, currentThread()) + expect(2) + delay(100) + assertEquals(mainThread, currentThread()) + expect(3) + } + } + assertEquals(testThread, currentThread()) + finish(4) + } + } + + @Test + fun testWithTimeoutContextDelayTimeout() { + if (testThread == mainThread) return // skip if already on the main thread + runTest { + expect(1) + assertFailsWith { + withTimeout(100) { + withContext(Dispatchers.Main) { + assertEquals(mainThread, currentThread()) + expect(2) + delay(1000) + expectUnreached() + } + } + expectUnreached() + } + assertEquals(testThread, currentThread()) + finish(3) + } + } + + @Test + fun testWithContextTimeoutDelayNoTimeout() { + if (testThread == mainThread) return // skip if already on the main thread + runTest { + expect(1) + withContext(Dispatchers.Main) { + withTimeout(1000) { + assertEquals(mainThread, currentThread()) + expect(2) + delay(100) + assertEquals(mainThread, currentThread()) + expect(3) + } + } + assertEquals(testThread, currentThread()) + finish(4) + } + } + + @Test + fun testWithContextTimeoutDelayTimeout() { + if (testThread == mainThread) return // skip if already on the main thread + runTest { + expect(1) + assertFailsWith { + withContext(Dispatchers.Main) { + withTimeout(100) { + assertEquals(mainThread, currentThread()) + expect(2) + delay(1000) + expectUnreached() + } + } + expectUnreached() + } + assertEquals(testThread, currentThread()) + finish(3) + } + } +} diff --git a/kotlinx-coroutines-core/native/src/Dispatchers.kt b/kotlinx-coroutines-core/nativeOther/src/Dispatchers.kt similarity index 68% rename from kotlinx-coroutines-core/native/src/Dispatchers.kt rename to kotlinx-coroutines-core/nativeOther/src/Dispatchers.kt index 6c650046a0..f728df6f8c 100644 --- a/kotlinx-coroutines-core/native/src/Dispatchers.kt +++ b/kotlinx-coroutines-core/nativeOther/src/Dispatchers.kt @@ -6,25 +6,14 @@ package kotlinx.coroutines import kotlin.coroutines.* -public actual object Dispatchers { - - public actual val Default: CoroutineDispatcher = createDefaultDispatcher() - - public actual val Main: MainCoroutineDispatcher = NativeMainDispatcher(Default) - - public actual val Unconfined: CoroutineDispatcher get() = kotlinx.coroutines.Unconfined // Avoid freezing -} +internal actual fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher = + NativeMainDispatcher(default) private class NativeMainDispatcher(val delegate: CoroutineDispatcher) : MainCoroutineDispatcher() { - override val immediate: MainCoroutineDispatcher - get() = throw UnsupportedOperationException("Immediate dispatching is not supported on Native") - + get() = throw UnsupportedOperationException("Immediate dispatching is not supported on this platform") override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block) - override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context) - override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.dispatchYield(context, block) - override fun toString(): String = delegate.toString() } diff --git a/kotlinx-coroutines-core/nativeOther/src/Thread.kt b/kotlinx-coroutines-core/nativeOther/src/Thread.kt new file mode 100644 index 0000000000..0034b3824f --- /dev/null +++ b/kotlinx-coroutines-core/nativeOther/src/Thread.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlin.native.concurrent.* + +internal actual fun initCurrentThread(): Thread = WorkerThread() + +internal actual fun Worker.toThread(): Thread = WorkerThread(this) diff --git a/kotlinx-coroutines-core/nativeOther/test/Launcher.kt b/kotlinx-coroutines-core/nativeOther/test/Launcher.kt index feddd4c097..3a762c83b2 100644 --- a/kotlinx-coroutines-core/nativeOther/test/Launcher.kt +++ b/kotlinx-coroutines-core/nativeOther/test/Launcher.kt @@ -20,4 +20,5 @@ fun mainBackground(args: Array) { // This is a separate entry point for tests with leak checker fun mainNoExit(args: Array) { testLauncherEntryPoint(args) + DefaultDispatcher.shutdown() } \ No newline at end of file diff --git a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt index 8507721e30..cab2870165 100644 --- a/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt +++ b/kotlinx-coroutines-debug/test/CoroutinesDumpTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.debug @@ -52,15 +52,16 @@ class CoroutinesDumpTest : DebugTestBase() { "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.activeMethod(CoroutinesDumpTest.kt:133)\n" + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest\$testRunningCoroutine\$1$deferred\$1.invokeSuspend(CoroutinesDumpTest.kt:41)\n" + "\t(Coroutine creation stacktrace)\n" + - "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + - "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + - "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" + - "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" + - "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt)\n" + - "\tat kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + - "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" + - "\tat kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + - "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.testRunningCoroutine(CoroutinesDumpTest.kt:49)", + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.startCoroutineImpl(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt.startCoroutineImpl(Unknown Source)\n" + + "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.testRunningCoroutine(CoroutinesDumpTest.kt)", ignoredCoroutine = "BlockingCoroutine" ) { deferred.cancel() @@ -78,19 +79,20 @@ class CoroutinesDumpTest : DebugTestBase() { awaitCoroutine() verifyDump( "Coroutine \"coroutine#1\":DeferredCoroutine{Active}@1e4a7dd4, state: RUNNING\n" + - "\tat java.lang.Thread.sleep(Native Method)\n" + - "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.nestedActiveMethod(CoroutinesDumpTest.kt:111)\n" + - "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.activeMethod(CoroutinesDumpTest.kt:106)\n" + - "\t(Coroutine creation stacktrace)\n" + - "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + - "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + - "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" + - "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" + + "\tat java.lang.Thread.sleep(Native Method)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.nestedActiveMethod(CoroutinesDumpTest.kt:111)\n" + + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.activeMethod(CoroutinesDumpTest.kt:106)\n" + + "\t(Coroutine creation stacktrace)\n" + + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.startCoroutineImpl(Builders.common.kt)\n" + + "\tat kotlinx.coroutines.BuildersKt.startCoroutineImpl(Unknown Source)\n" + + "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt)\n" + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt)\n" + "\tat kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" + "\tat kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + - "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.testRunningCoroutineWithSuspensionPoint(CoroutinesDumpTest.kt:71)", + "\tat kotlinx.coroutines.debug.CoroutinesDumpTest.testRunningCoroutineWithSuspensionPoint(CoroutinesDumpTest.kt)", ignoredCoroutine = "BlockingCoroutine" ) { deferred.cancel() @@ -113,18 +115,18 @@ class CoroutinesDumpTest : DebugTestBase() { deferred.cancel() coroutineThread!!.interrupt() - val expected = - ("kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + - "kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + - "kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:109)\n" + - "kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:160)\n" + - "kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt:88)\n" + - "kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + - "kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt:81)\n" + - "kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + - "kotlinx.coroutines.debug.CoroutinesDumpTest\$testCreationStackTrace\$1.invokeSuspend(CoroutinesDumpTest.kt)").trimStackTrace() - assertTrue(result.startsWith(expected)) + ("kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt)\n" + + "kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt)\n" + + "kotlinx.coroutines.BuildersKt__Builders_commonKt.startCoroutineImpl(Builders.common.kt)\n" + + "kotlinx.coroutines.BuildersKt.startCoroutineImpl(Unknown Source)\n" + + "kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt)\n" + + "kotlinx.coroutines.BuildersKt__Builders_commonKt.async(Builders.common.kt)\n" + + "kotlinx.coroutines.BuildersKt.async(Unknown Source)\n" + + "kotlinx.coroutines.BuildersKt__Builders_commonKt.async\$default(Builders.common.kt)\n" + + "kotlinx.coroutines.BuildersKt.async\$default(Unknown Source)\n" + + "kotlinx.coroutines.debug.CoroutinesDumpTest\$testCreationStackTrace\$1.invokeSuspend(CoroutinesDumpTest.kt)").trimStackTrace() + assertTrue(result.startsWith(expected), "Actual:\n$result") } @Test @@ -180,4 +182,15 @@ class CoroutinesDumpTest : DebugTestBase() { (monitor as Object).notifyAll() } } + + private fun assertStartsWith(expected: String, actual: String) { + if (!actual.startsWith(expected)) { + println("----- Expected prefix") + println(expected) + println("----- Actual") + println(actual) + println("-----") + assertEquals(expected, actual) + } + } } diff --git a/kotlinx-coroutines-debug/test/DebugProbesTest.kt b/kotlinx-coroutines-debug/test/DebugProbesTest.kt index 24050e563c..ab341546b1 100644 --- a/kotlinx-coroutines-debug/test/DebugProbesTest.kt +++ b/kotlinx-coroutines-debug/test/DebugProbesTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.coroutines.debug @@ -45,16 +45,17 @@ class DebugProbesTest : DebugTestBase() { "\tat kotlinx.coroutines.DeferredCoroutine.await\$suspendImpl(Builders.common.kt)\n" + "\tat kotlinx.coroutines.debug.DebugProbesTest.oneMoreNestedMethod(DebugProbesTest.kt:71)\n" + "\tat kotlinx.coroutines.debug.DebugProbesTest.nestedMethod(DebugProbesTest.kt:66)\n" + - "\t(Coroutine creation stacktrace)\n" + + "\t(Coroutine creation stacktrace)\n" + "\tat kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:116)\n" + - "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:23)\n" + - "\tat kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:99)\n" + - "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:148)\n" + - "\tat kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:45)\n" + + "\tat kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26)\n" + + "\tat kotlinx.coroutines.BuildersKt__Builders_commonKt.startCoroutineImpl(Builders.common.kt:179)\n" + + "\tat kotlinx.coroutines.BuildersKt.startCoroutineImpl(Unknown Source)\n" + + "\tat kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:145)\n" + + "\tat kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:55)\n" + "\tat kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)\n" + - "\tat kotlinx.coroutines.TestBase.runTest(TestBase.kt:138)\n" + - "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt:19)\n" + - "\tat kotlinx.coroutines.debug.DebugProbesTest.testAsyncWithProbes(DebugProbesTest.kt:38)", + "\tat kotlinx.coroutines.TestBase.runTest(TestBase.kt:188)\n" + + "\tat kotlinx.coroutines.TestBase.runTest\$default(TestBase.kt:182)\n" + + "\tat kotlinx.coroutines.debug.DebugProbesTest.testAsyncWithProbes(DebugProbesTest.kt:39)", "Caused by: java.util.concurrent.ExecutionException\n" + "\tat kotlinx.coroutines.debug.DebugProbesTest\$createDeferred\$1.invokeSuspend(DebugProbesTest.kt:16)\n" + "\tat kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)\n") diff --git a/kotlinx-coroutines-debug/test/StracktraceUtils.kt b/kotlinx-coroutines-debug/test/StracktraceUtils.kt index 8c591ebd44..99a8b77fee 100644 --- a/kotlinx-coroutines-debug/test/StracktraceUtils.kt +++ b/kotlinx-coroutines-debug/test/StracktraceUtils.kt @@ -113,7 +113,14 @@ public fun verifyDump(vararg traces: String, ignoredCoroutine: String? = null) { val actualLines = cleanBlockHoundTraces(actualTrace.split("\n")) val expectedLines = expectedTrace.split("\n") for (i in expectedLines.indices) { - assertEquals(expectedLines[i], actualLines[i]) + if (expectedLines[i] != actualLines[i]) { + println("----- Expected") + expectedLines.forEach { println(it) } + println("----- Actual") + actualLines.forEach { println(it) } + println("-----") + assertEquals(expectedLines[i], actualLines[i]) + } } } }