Skip to content

Commit c038e59

Browse files
committed
Add the contract to runBlocking for shared JVM/Native code
Additionally, on Native, make thread keepalive checks a bit more efficient.
1 parent d3f1f23 commit c038e59

File tree

8 files changed

+86
-160
lines changed

8 files changed

+86
-160
lines changed

kotlinx-coroutines-core/common/src/EventLoop.common.kt

-7
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,6 @@ internal abstract class EventLoop : CoroutineDispatcher() {
6565
task.run()
6666
return true
6767
}
68-
/**
69-
* Returns `true` if the invoking `runBlocking(context) { ... }` that was passed this event loop in its context
70-
* parameter should call [processNextEvent] for this event loop (otherwise, it will process thread-local one).
71-
* By default, event loop implementation is thread-local and should not processed in the context
72-
* (current thread's event loop should be processed instead).
73-
*/
74-
open fun shouldBeProcessedFromContext(): Boolean = false
7568

7669
/**
7770
* Dispatches task whose dispatcher returned `false` from [CoroutineDispatcher.isDispatchNeeded]

kotlinx-coroutines-core/common/src/internal/Concurrent.common.kt

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package kotlinx.coroutines.internal
22

3-
internal expect class ReentrantLock() {
4-
fun tryLock(): Boolean
5-
fun unlock()
6-
}
3+
internal expect class ReentrantLock()
74

85
internal expect inline fun <T> ReentrantLock.withLock(action: () -> T): T
96

kotlinx-coroutines-core/concurrent/src/Builders.concurrent.kt

+49-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
@file:JvmMultifileClass
2+
@file:JvmName("BuildersKt")
3+
@file:Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND")
4+
15
package kotlinx.coroutines
26

7+
import kotlin.contracts.ExperimentalContracts
8+
import kotlin.contracts.InvocationKind
9+
import kotlin.contracts.contract
310
import kotlin.coroutines.*
11+
import kotlin.jvm.JvmMultifileClass
12+
import kotlin.jvm.JvmName
413

514
/**
615
* Runs a new coroutine and **blocks** the current thread until its completion.
@@ -20,5 +29,44 @@ import kotlin.coroutines.*
2029
*
2130
* Here, instead of releasing the thread on which `loadConfiguration` runs if `fetchConfigurationData` suspends, it will
2231
* block, potentially leading to thread starvation issues.
32+
*
33+
* The default [CoroutineDispatcher] for this builder is an internal implementation of event loop that processes continuations
34+
* in this blocked thread until the completion of this coroutine.
35+
* See [CoroutineDispatcher] for the other implementations that are provided by `kotlinx.coroutines`.
36+
*
37+
* When [CoroutineDispatcher] is explicitly specified in the [context], then the new coroutine runs in the context of
38+
* the specified dispatcher while the current thread is blocked. If the specified dispatcher is an event loop of another `runBlocking`,
39+
* then this invocation uses the outer event loop.
40+
*
41+
* If this blocked thread is interrupted (see `Thread.interrupt`), then the coroutine job is cancelled and
42+
* this `runBlocking` invocation throws `InterruptedException`.
43+
*
44+
* See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging facilities that are available
45+
* for a newly created coroutine.
46+
*
47+
* @param context the context of the coroutine. The default value is an event loop on the current thread.
48+
* @param block the coroutine code.
2349
*/
24-
public expect fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
50+
@OptIn(ExperimentalContracts::class)
51+
public fun <T> runBlocking(
52+
context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T
53+
): T {
54+
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
55+
val contextInterceptor = context[ContinuationInterceptor]
56+
val eventLoop: EventLoop?
57+
val newContext: CoroutineContext
58+
if (contextInterceptor == null) {
59+
// create or use private event loop if no dispatcher is specified
60+
eventLoop = ThreadLocalEventLoop.eventLoop
61+
newContext = GlobalScope.newCoroutineContext(context + eventLoop)
62+
} else {
63+
eventLoop = ThreadLocalEventLoop.currentOrNull()
64+
newContext = GlobalScope.newCoroutineContext(context)
65+
}
66+
return runBlockingImpl(newContext, eventLoop, block)
67+
}
68+
69+
/** We can't inline it, because an `expect fun` can't have contracts. */
70+
internal expect fun <T> runBlockingImpl(
71+
newContext: CoroutineContext, eventLoop: EventLoop?, block: suspend CoroutineScope.() -> T
72+
): T

kotlinx-coroutines-core/concurrent/test/RunBlockingTest.kt

+10
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,14 @@ class RunBlockingTest : TestBase() {
194194
}
195195
}
196196
}
197+
198+
/** Will not compile if [runBlocking] doesn't have the "runs exactly once" contract. */
199+
@Test
200+
fun testContract() {
201+
val rb: Int
202+
runBlocking {
203+
rb = 42
204+
}
205+
rb.hashCode() // unused
206+
}
197207
}

kotlinx-coroutines-core/jvm/src/Builders.kt

+4-60
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,15 @@
11
@file:JvmMultifileClass
22
@file:JvmName("BuildersKt")
3-
@file:OptIn(ExperimentalContracts::class)
4-
@file:Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND")
53

64
package kotlinx.coroutines
75

8-
import java.util.concurrent.locks.*
9-
import kotlin.contracts.*
106
import kotlin.coroutines.*
117

12-
/**
13-
* Runs a new coroutine and **blocks** the current thread _interruptibly_ until its completion.
14-
*
15-
* It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in
16-
* `main` functions and in tests.
17-
*
18-
* Calling [runBlocking] from a suspend function is redundant.
19-
* For example, the following code is incorrect:
20-
* ```
21-
* suspend fun loadConfiguration() {
22-
* // DO NOT DO THIS:
23-
* val data = runBlocking { // <- redundant and blocks the thread, do not do that
24-
* fetchConfigurationData() // suspending function
25-
* }
26-
* ```
27-
*
28-
* Here, instead of releasing the thread on which `loadConfiguration` runs if `fetchConfigurationData` suspends, it will
29-
* block, potentially leading to thread starvation issues.
30-
*
31-
* The default [CoroutineDispatcher] for this builder is an internal implementation of event loop that processes continuations
32-
* in this blocked thread until the completion of this coroutine.
33-
* See [CoroutineDispatcher] for the other implementations that are provided by `kotlinx.coroutines`.
34-
*
35-
* When [CoroutineDispatcher] is explicitly specified in the [context], then the new coroutine runs in the context of
36-
* the specified dispatcher while the current thread is blocked. If the specified dispatcher is an event loop of another `runBlocking`,
37-
* then this invocation uses the outer event loop.
38-
*
39-
* If this blocked thread is interrupted (see [Thread.interrupt]), then the coroutine job is cancelled and
40-
* this `runBlocking` invocation throws [InterruptedException].
41-
*
42-
* See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging facilities that are available
43-
* for a newly created coroutine.
44-
*
45-
* @param context the context of the coroutine. The default value is an event loop on the current thread.
46-
* @param block the coroutine code.
47-
*/
488
@Throws(InterruptedException::class)
49-
public actual fun <T> runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T {
50-
contract {
51-
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
52-
}
53-
val currentThread = Thread.currentThread()
54-
val contextInterceptor = context[ContinuationInterceptor]
55-
val eventLoop: EventLoop?
56-
val newContext: CoroutineContext
57-
if (contextInterceptor == null) {
58-
// create or use private event loop if no dispatcher is specified
59-
eventLoop = ThreadLocalEventLoop.eventLoop
60-
newContext = GlobalScope.newCoroutineContext(context + eventLoop)
61-
} else {
62-
// See if context's interceptor is an event loop that we shall use (to support TestContext)
63-
// or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
64-
eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
65-
?: ThreadLocalEventLoop.currentOrNull()
66-
newContext = GlobalScope.newCoroutineContext(context)
67-
}
68-
val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
9+
internal actual fun <T> runBlockingImpl(
10+
newContext: CoroutineContext, eventLoop: EventLoop?, block: suspend CoroutineScope.() -> T
11+
): T {
12+
val coroutine = BlockingCoroutine<T>(newContext, Thread.currentThread(), eventLoop)
6913
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
7014
return coroutine.joinBlocking()
7115
}

kotlinx-coroutines-core/jvm/test-resources/stacktraces/channels/testSendToChannel.txt

+3-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ Caused by: java.util.concurrent.CancellationException: Channel was cancelled
1515
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt)
1616
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt)
1717
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt)
18-
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt)
18+
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlockingImpl(Builders.kt)
19+
at kotlinx.coroutines.BuildersKt.runBlockingImpl(Unknown Source)
20+
at kotlinx.coroutines.BuildersKt__Builders_concurrentKt.runBlocking(Builders.concurrent.kt)
1921
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
2022
at kotlinx.coroutines.testing.TestBase.runTest(TestBase.kt)

kotlinx-coroutines-core/jvm/test/RunBlockingJvmTest.kt

-16
This file was deleted.

kotlinx-coroutines-core/native/src/Builders.kt

+19-71
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,49 @@
1-
@file:OptIn(ExperimentalContracts::class, ObsoleteWorkersApi::class)
2-
@file:Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND")
1+
@file:OptIn(ObsoleteWorkersApi::class)
32

43
package kotlinx.coroutines
54

6-
import kotlinx.cinterop.*
7-
import kotlin.contracts.*
85
import kotlin.coroutines.*
96
import kotlin.native.concurrent.*
107

11-
/**
12-
* Runs a new coroutine and **blocks** the current thread _interruptibly_ until its completion.
13-
*
14-
* It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in
15-
* `main` functions and in tests.
16-
*
17-
* Calling [runBlocking] from a suspend function is redundant.
18-
* For example, the following code is incorrect:
19-
* ```
20-
* suspend fun loadConfiguration() {
21-
* // DO NOT DO THIS:
22-
* val data = runBlocking { // <- redundant and blocks the thread, do not do that
23-
* fetchConfigurationData() // suspending function
24-
* }
25-
* ```
26-
*
27-
* Here, instead of releasing the thread on which `loadConfiguration` runs if `fetchConfigurationData` suspends, it will
28-
* block, potentially leading to thread starvation issues.
29-
*
30-
* The default [CoroutineDispatcher] for this builder is an internal implementation of event loop that processes continuations
31-
* in this blocked thread until the completion of this coroutine.
32-
* See [CoroutineDispatcher] for the other implementations that are provided by `kotlinx.coroutines`.
33-
*
34-
* When [CoroutineDispatcher] is explicitly specified in the [context], then the new coroutine runs in the context of
35-
* the specified dispatcher while the current thread is blocked. If the specified dispatcher is an event loop of another `runBlocking`,
36-
* then this invocation uses the outer event loop.
37-
*
38-
* If this blocked thread is interrupted (see [Thread.interrupt]), then the coroutine job is cancelled and
39-
* this `runBlocking` invocation throws [InterruptedException].
40-
*
41-
* See [newCoroutineContext][CoroutineScope.newCoroutineContext] for a description of debugging facilities that are available
42-
* for a newly created coroutine.
43-
*
44-
* @param context the context of the coroutine. The default value is an event loop on the current thread.
45-
* @param block the coroutine code.
46-
*/
47-
public actual fun <T> runBlocking(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T {
48-
contract {
49-
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
50-
}
51-
val contextInterceptor = context[ContinuationInterceptor]
52-
val eventLoop: EventLoop?
53-
val newContext: CoroutineContext
54-
if (contextInterceptor == null) {
55-
// create or use private event loop if no dispatcher is specified
56-
eventLoop = ThreadLocalEventLoop.eventLoop
57-
newContext = GlobalScope.newCoroutineContext(context + eventLoop)
58-
} else {
59-
// See if context's interceptor is an event loop that we shall use (to support TestContext)
60-
// or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
61-
eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
62-
?: ThreadLocalEventLoop.currentOrNull()
63-
newContext = GlobalScope.newCoroutineContext(context)
64-
}
65-
val coroutine = BlockingCoroutine<T>(newContext, eventLoop)
66-
var completed = false
67-
ThreadLocalKeepAlive.addCheck { !completed }
8+
internal actual fun <T> runBlockingImpl(
9+
newContext: CoroutineContext, eventLoop: EventLoop?, block: suspend CoroutineScope.() -> T
10+
): T {
11+
val coroutine = BlockingCoroutine<T>(newContext, Worker.current, eventLoop)
12+
ThreadLocalKeepAlive.registerUsage()
6813
try {
6914
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
7015
return coroutine.joinBlocking()
7116
} finally {
72-
completed = true
17+
ThreadLocalKeepAlive.unregisterUsage()
7318
}
7419
}
7520

7621
@ThreadLocal
7722
private object ThreadLocalKeepAlive {
78-
/** If any of these checks passes, this means this [Worker] is still used. */
79-
private var checks = mutableListOf<() -> Boolean>()
23+
/** If larger than 0, this means this [Worker] is still used. */
24+
private var usages = 0
8025

8126
/** Whether the worker currently tries to keep itself alive. */
8227
private var keepAliveLoopActive = false
8328

84-
/** Adds another stopgap that must be passed before the [Worker] can be terminated. */
85-
fun addCheck(terminationForbidden: () -> Boolean) {
86-
checks.add(terminationForbidden)
29+
/** Ensure that the worker is kept alive until the matching [unregisterUsage] is called. */
30+
fun registerUsage() {
31+
usages++
8732
if (!keepAliveLoopActive) keepAlive()
8833
}
8934

35+
/** Undo [registerUsage]. */
36+
fun unregisterUsage() {
37+
usages--
38+
}
39+
9040
/**
9141
* Send a ping to the worker to prevent it from terminating while this coroutine is running,
9242
* ensuring that continuations don't get dropped and forgotten.
9343
*/
9444
private fun keepAlive() {
95-
// only keep the checks that still forbid the termination
96-
checks = checks.filter { it() }.toMutableList()
9745
// if there are no checks left, we no longer keep the worker alive, it can be terminated
98-
keepAliveLoopActive = checks.isNotEmpty()
46+
keepAliveLoopActive = usages > 0
9947
if (keepAliveLoopActive) {
10048
Worker.current.executeAfter(afterMicroseconds = 100_000) {
10149
keepAlive()
@@ -106,9 +54,9 @@ private object ThreadLocalKeepAlive {
10654

10755
private class BlockingCoroutine<T>(
10856
parentContext: CoroutineContext,
57+
private val joinWorker: Worker,
10958
private val eventLoop: EventLoop?
11059
) : AbstractCoroutine<T>(parentContext, true, true) {
111-
private val joinWorker = Worker.current
11260

11361
override val isScopedCoroutine: Boolean get() = true
11462

0 commit comments

Comments
 (0)