Skip to content

Commit e5a7b42

Browse files
authored
Add the contract to runBlocking for shared JVM/Native code (#4368)
Additionally, on Native, make thread keepalive checks a bit more efficient.
1 parent 8b32c4e commit e5a7b42

File tree

11 files changed

+140
-150
lines changed

11 files changed

+140
-150
lines changed

integration-testing/build.gradle.kts

+17-1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,15 @@ sourceSets {
134134
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
135135
}
136136
}
137+
138+
create("javaConsumersTest") {
139+
compileClasspath += sourceSets.test.get().runtimeClasspath
140+
runtimeClasspath += sourceSets.test.get().runtimeClasspath
141+
142+
dependencies {
143+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
144+
}
145+
}
137146
}
138147

139148
kotlin {
@@ -199,16 +208,23 @@ tasks {
199208
classpath = sourceSet.runtimeClasspath
200209
}
201210

211+
create<Test>("javaConsumersTest") {
212+
val sourceSet = sourceSets[name]
213+
testClassesDirs = sourceSet.output.classesDirs
214+
classpath = sourceSet.runtimeClasspath
215+
}
216+
202217
check {
203218
dependsOn(
204219
"jvmCoreTest",
205220
"debugDynamicAgentTest",
206221
"mavenTest",
207222
"debugAgentTest",
208223
"coreAgentTest",
224+
"javaConsumersTest",
209225
":jpmsTest:check",
210226
"smokeTest:build",
211-
"java8Test:check"
227+
"java8Test:check",
212228
)
213229
}
214230

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import kotlinx.coroutines.BuildersKt;
2+
import kotlinx.coroutines.Dispatchers;
3+
import org.junit.Test;
4+
import org.junit.Assert;
5+
6+
public class RunBlockingJavaTest {
7+
Boolean entered = false;
8+
9+
/** This code will not compile if `runBlocking` doesn't declare `@Throws(InterruptedException::class)` */
10+
@Test
11+
public void testRunBlocking() {
12+
try {
13+
BuildersKt.runBlocking(Dispatchers.getIO(), (scope, continuation) -> {
14+
entered = true;
15+
return null;
16+
});
17+
} catch (InterruptedException e) {
18+
}
19+
Assert.assertTrue(entered);
20+
}
21+
}

kotlinx-coroutines-core/api/kotlinx-coroutines-core.api

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public final class kotlinx/coroutines/BuildersKt {
2727
public static synthetic fun launch$default (Lkotlinx/coroutines/CoroutineScope;Lkotlin/coroutines/CoroutineContext;Lkotlinx/coroutines/CoroutineStart;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/Job;
2828
public static final fun runBlocking (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
2929
public static synthetic fun runBlocking$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/lang/Object;
30+
public static final fun runBlockingK (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
31+
public static synthetic fun runBlockingK$default (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/lang/Object;
3032
public static final fun withContext (Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
3133
}
3234

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

+50-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
@file:Suppress("LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND")
2+
@file:JvmMultifileClass
3+
@file:JvmName("BuildersKt")
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,45 @@ 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+
@JvmName("runBlockingK")
52+
public fun <T> runBlocking(
53+
context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T
54+
): T {
55+
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
56+
val contextInterceptor = context[ContinuationInterceptor]
57+
val eventLoop: EventLoop?
58+
val newContext: CoroutineContext
59+
if (contextInterceptor == null) {
60+
// create or use private event loop if no dispatcher is specified
61+
eventLoop = ThreadLocalEventLoop.eventLoop
62+
newContext = GlobalScope.newCoroutineContext(context + eventLoop)
63+
} else {
64+
eventLoop = ThreadLocalEventLoop.currentOrNull()
65+
newContext = GlobalScope.newCoroutineContext(context)
66+
}
67+
return runBlockingImpl(newContext, eventLoop, block)
68+
}
69+
70+
/** We can't inline it, because an `expect fun` can't have contracts. */
71+
internal expect fun <T> runBlockingImpl(
72+
newContext: CoroutineContext, eventLoop: EventLoop?, block: suspend CoroutineScope.() -> T
73+
): 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

+16-56
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,31 @@
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

128
/**
13-
* Runs a new coroutine and **blocks** the current thread _interruptibly_ until its completion.
9+
* The same as [runBlocking], but for consumption from Java.
10+
* From Kotlin's point of view, this function has the exact same signature as the regular [runBlocking].
11+
* This is done so that it can not be called from Kotlin, despite the fact that it is public.
1412
*
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.
13+
* We do not expose this [runBlocking] in the documentation, because it is not supposed to be used from Kotlin.
1714
*
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.
15+
* @suppress
4716
*/
4817
@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)
18+
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
19+
@kotlin.internal.LowPriorityInOverloadResolution
20+
public fun <T> runBlocking(
21+
context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T
22+
): T = runBlocking(context, block)
23+
24+
@Throws(InterruptedException::class)
25+
internal actual fun <T> runBlockingImpl(
26+
newContext: CoroutineContext, eventLoop: EventLoop?, block: suspend CoroutineScope.() -> T
27+
): T {
28+
val coroutine = BlockingCoroutine<T>(newContext, Thread.currentThread(), eventLoop)
6929
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
7030
return coroutine.joinBlocking()
7131
}

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

+4-2
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)
19-
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
18+
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlockingImpl(Builders.kt)
19+
at kotlinx.coroutines.BuildersKt.runBlockingImpl(Unknown Source)
20+
at kotlinx.coroutines.BuildersKt__Builders_concurrentKt.runBlockingK(Builders.concurrent.kt)
21+
at kotlinx.coroutines.BuildersKt.runBlockingK(Unknown Source)
2022
at kotlinx.coroutines.testing.TestBase.runTest(TestBase.kt)

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

-8
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,6 @@ import kotlin.test.*
88
import kotlin.time.Duration
99

1010
class RunBlockingJvmTest : TestBase() {
11-
@Test
12-
fun testContract() {
13-
val rb: Int
14-
runBlocking {
15-
rb = 42
16-
}
17-
rb.hashCode() // unused
18-
}
1911

2012
/** Tests that the [runBlocking] coroutine runs to completion even it was interrupted. */
2113
@Test

0 commit comments

Comments
 (0)