Skip to content

Commit 2272c37

Browse files
committed
Introduce extensions on CoroutineScope to better reflect its semantics
Fixes #828
1 parent c26e071 commit 2272c37

File tree

3 files changed

+86
-5
lines changed

3 files changed

+86
-5
lines changed

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

+46-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ package kotlinx.coroutines
66

77
import kotlinx.coroutines.internal.*
88
import kotlinx.coroutines.intrinsics.*
9-
import kotlin.coroutines.intrinsics.*
109
import kotlin.coroutines.*
10+
import kotlin.coroutines.intrinsics.*
1111

1212
/**
1313
* Defines a scope for new coroutines. Every coroutine builder
@@ -73,6 +73,51 @@ public interface CoroutineScope {
7373
public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =
7474
ContextScope(coroutineContext + context)
7575

76+
/**
77+
* Returns the [Job] object related to current coroutine scope or throws [IllegalStateException] if the scope does not have one.
78+
* Resulting [Job] is bound with the lifecycle of the given scope: [CoroutineScope.cancel] matches [Job.cancel],
79+
* [CoroutineScope.join] matches [Job.join] etc.
80+
* Any coroutine launched by this scope will become a child of the resulting job.
81+
*/
82+
public val CoroutineScope.job: Job get() = coroutineContext[Job] ?: error("Scope $this does not have job in it")
83+
84+
/**
85+
* Returns the [Job] object related to current coroutine scope or `null` if the scope does not have one.
86+
* See [CoroutineScope.job] for a more detailed explanation of the scope job.
87+
* This method is not recommended to be used in a general application code, application scope should always have a job associated with it.
88+
*/
89+
public val CoroutineScope.jobOrNull: Job? get() = coroutineContext[Job]
90+
91+
/**
92+
* Returns the [CoroutineDispatcher] object related to current coroutine scope or throws [IllegalStateException] if the scope does not have one.
93+
* Resulting [CoroutineDispatcher] is the dispatcher where all scope children are executed if they do not override dispatcher.
94+
* E.g.
95+
* ```
96+
* scope.launch { ... } // Will be executed in scope.dispatcher (or Dispatchers.Default if scope does not have a dispatcher)
97+
* scope.launch(Dispatchers.IO) { ... } // Will be executed in IO dispatcher, but still belongs to the scope
98+
* ```
99+
*/
100+
public val CoroutineScope.dispatcher: CoroutineDispatcher get() = coroutineContext[ContinuationInterceptor] as? CoroutineDispatcher ?: error("Scope $this does not have dispatcher in it")
101+
102+
/**
103+
* Returns the [CoroutineDispatcher] object related to current coroutine scope or `null` if scope does not have one.
104+
* See [CoroutineScope.dispatcher] for a more detailed explanation of scope dispatcher.
105+
*/
106+
public val CoroutineScope.dispatcherOrNull: CoroutineDispatcher? get() = coroutineContext[ContinuationInterceptor] as? CoroutineDispatcher
107+
108+
/**
109+
* Cancels this scope, including its [job] and all its children.
110+
*/
111+
public fun CoroutineScope.cancel(): Unit = coroutineContext.cancel()
112+
113+
/**
114+
* Suspends coroutine until this scope is complete, including all its children.
115+
* For cancellation semantics see [Job.join].
116+
*/
117+
public suspend fun CoroutineScope.join() {
118+
jobOrNull?.join()
119+
}
120+
76121
/**
77122
* Returns `true` when current [Job] is still active (has not completed and was not cancelled yet).
78123
*
@@ -88,7 +133,6 @@ public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineSco
88133
* See [coroutineContext][kotlin.coroutines.coroutineContext],
89134
* [isActive][kotlinx.coroutines.isActive] and [Job.isActive].
90135
*/
91-
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
92136
public val CoroutineScope.isActive: Boolean
93137
get() = coroutineContext[Job]?.isActive ?: true
94138

common/kotlinx-coroutines-core-common/test/CoroutineScopeTest.kt

+37
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ class CoroutineScopeTest : TestBase() {
1414
@Test
1515
fun testScope() = runTest {
1616
suspend fun callJobScoped() = coroutineScope {
17+
assertNotNull(jobOrNull)
18+
assertNotNull(dispatcher)
19+
assertSame(coroutineContext[ContinuationInterceptor], dispatcher)
20+
assertSame(coroutineContext[Job], job)
21+
1722
expect(2)
1823
launch {
1924
expect(4)
@@ -245,6 +250,28 @@ class CoroutineScopeTest : TestBase() {
245250
finish(7)
246251
}
247252

253+
@Test
254+
fun testCancelAndJoin() = runTest {
255+
val scope = CoroutineScope(coroutineContext + Job())
256+
expect(1)
257+
scope.launch {
258+
expect(3)
259+
try {
260+
delay(Long.MAX_VALUE)
261+
} finally {
262+
expect(6)
263+
}
264+
}
265+
266+
expect(2)
267+
yield()
268+
expect(4)
269+
scope.cancel()
270+
expect(5)
271+
scope.join()
272+
finish(7)
273+
}
274+
248275
@Test
249276
fun testScopePlusContext() {
250277
assertSame(EmptyCoroutineContext, scopePlusContext(EmptyCoroutineContext, EmptyCoroutineContext))
@@ -256,6 +283,16 @@ class CoroutineScopeTest : TestBase() {
256283
assertSame(Dispatchers.Unconfined, scopePlusContext(Dispatchers.Unconfined, Dispatchers.Unconfined))
257284
}
258285

286+
@Test
287+
fun testScopeProperties() {
288+
assertFailsWith<IllegalStateException> { CoroutineScope(EmptyCoroutineContext).dispatcher }
289+
assertFailsWith<IllegalStateException> { ContextScope(EmptyCoroutineContext).job }
290+
assertNull(CoroutineScope(EmptyCoroutineContext).dispatcherOrNull)
291+
assertNotNull(CoroutineScope(EmptyCoroutineContext).job)
292+
assertSame(Dispatchers.Unconfined, CoroutineScope(Unconfined).dispatcher)
293+
assertSame(Dispatchers.Unconfined, CoroutineScope(Unconfined).dispatcherOrNull)
294+
}
295+
259296
private fun scopePlusContext(c1: CoroutineContext, c2: CoroutineContext) =
260297
(ContextScope(c1) + c2).coroutineContext
261298
}

core/kotlinx-coroutines-core/test/scheduling/CoroutineSchedulerCloseStressTest.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class CoroutineSchedulerCloseStressTest(private val mode: Mode) : TestBase() {
3030
private val rnd = Random()
3131

3232
private lateinit var closeableDispatcher: ExperimentalCoroutineDispatcher
33-
private lateinit var dispatcher: ExecutorCoroutineDispatcher
33+
private lateinit var coroutineDispatcher: ExecutorCoroutineDispatcher
3434
private var closeIndex = -1
3535

3636
private val started = atomic(0)
@@ -55,14 +55,14 @@ class CoroutineSchedulerCloseStressTest(private val mode: Mode) : TestBase() {
5555

5656
private fun launchCoroutines() = runBlocking {
5757
closeableDispatcher = ExperimentalCoroutineDispatcher(N_THREADS)
58-
dispatcher = when (mode) {
58+
coroutineDispatcher = when (mode) {
5959
Mode.CPU -> closeableDispatcher
6060
Mode.CPU_LIMITED -> closeableDispatcher.limited(N_THREADS) as ExecutorCoroutineDispatcher
6161
Mode.BLOCKING -> closeableDispatcher.blocking(N_THREADS) as ExecutorCoroutineDispatcher
6262
}
6363
started.value = 0
6464
finished.value = 0
65-
withContext(dispatcher) {
65+
withContext(coroutineDispatcher) {
6666
launchChild(0, 0)
6767
}
6868
assertEquals(N_COROS, started.value)

0 commit comments

Comments
 (0)