Skip to content

Commit

Permalink
Introduce extensions on CoroutineScope to better reflect its semantics
Browse files Browse the repository at this point in the history
Fixes #828
  • Loading branch information
qwwdfsad committed Nov 14, 2018
1 parent c26e071 commit 2272c37
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 5 deletions.
48 changes: 46 additions & 2 deletions common/kotlinx-coroutines-core-common/src/CoroutineScope.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ package kotlinx.coroutines

import kotlinx.coroutines.internal.*
import kotlinx.coroutines.intrinsics.*
import kotlin.coroutines.intrinsics.*
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*

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

/**
* Returns the [Job] object related to current coroutine scope or throws [IllegalStateException] if the scope does not have one.
* Resulting [Job] is bound with the lifecycle of the given scope: [CoroutineScope.cancel] matches [Job.cancel],
* [CoroutineScope.join] matches [Job.join] etc.
* Any coroutine launched by this scope will become a child of the resulting job.
*/
public val CoroutineScope.job: Job get() = coroutineContext[Job] ?: error("Scope $this does not have job in it")

/**
* Returns the [Job] object related to current coroutine scope or `null` if the scope does not have one.
* See [CoroutineScope.job] for a more detailed explanation of the scope job.
* This method is not recommended to be used in a general application code, application scope should always have a job associated with it.
*/
public val CoroutineScope.jobOrNull: Job? get() = coroutineContext[Job]

/**
* Returns the [CoroutineDispatcher] object related to current coroutine scope or throws [IllegalStateException] if the scope does not have one.
* Resulting [CoroutineDispatcher] is the dispatcher where all scope children are executed if they do not override dispatcher.
* E.g.
* ```
* scope.launch { ... } // Will be executed in scope.dispatcher (or Dispatchers.Default if scope does not have a dispatcher)
* scope.launch(Dispatchers.IO) { ... } // Will be executed in IO dispatcher, but still belongs to the scope
* ```
*/
public val CoroutineScope.dispatcher: CoroutineDispatcher get() = coroutineContext[ContinuationInterceptor] as? CoroutineDispatcher ?: error("Scope $this does not have dispatcher in it")

/**
* Returns the [CoroutineDispatcher] object related to current coroutine scope or `null` if scope does not have one.
* See [CoroutineScope.dispatcher] for a more detailed explanation of scope dispatcher.
*/
public val CoroutineScope.dispatcherOrNull: CoroutineDispatcher? get() = coroutineContext[ContinuationInterceptor] as? CoroutineDispatcher

/**
* Cancels this scope, including its [job] and all its children.
*/
public fun CoroutineScope.cancel(): Unit = coroutineContext.cancel()

/**
* Suspends coroutine until this scope is complete, including all its children.
* For cancellation semantics see [Job.join].
*/
public suspend fun CoroutineScope.join() {
jobOrNull?.join()
}

/**
* Returns `true` when current [Job] is still active (has not completed and was not cancelled yet).
*
Expand All @@ -88,7 +133,6 @@ public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineSco
* See [coroutineContext][kotlin.coroutines.coroutineContext],
* [isActive][kotlinx.coroutines.isActive] and [Job.isActive].
*/
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
public val CoroutineScope.isActive: Boolean
get() = coroutineContext[Job]?.isActive ?: true

Expand Down
37 changes: 37 additions & 0 deletions common/kotlinx-coroutines-core-common/test/CoroutineScopeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ class CoroutineScopeTest : TestBase() {
@Test
fun testScope() = runTest {
suspend fun callJobScoped() = coroutineScope {
assertNotNull(jobOrNull)
assertNotNull(dispatcher)
assertSame(coroutineContext[ContinuationInterceptor], dispatcher)
assertSame(coroutineContext[Job], job)

expect(2)
launch {
expect(4)
Expand Down Expand Up @@ -245,6 +250,28 @@ class CoroutineScopeTest : TestBase() {
finish(7)
}

@Test
fun testCancelAndJoin() = runTest {
val scope = CoroutineScope(coroutineContext + Job())
expect(1)
scope.launch {
expect(3)
try {
delay(Long.MAX_VALUE)
} finally {
expect(6)
}
}

expect(2)
yield()
expect(4)
scope.cancel()
expect(5)
scope.join()
finish(7)
}

@Test
fun testScopePlusContext() {
assertSame(EmptyCoroutineContext, scopePlusContext(EmptyCoroutineContext, EmptyCoroutineContext))
Expand All @@ -256,6 +283,16 @@ class CoroutineScopeTest : TestBase() {
assertSame(Dispatchers.Unconfined, scopePlusContext(Dispatchers.Unconfined, Dispatchers.Unconfined))
}

@Test
fun testScopeProperties() {
assertFailsWith<IllegalStateException> { CoroutineScope(EmptyCoroutineContext).dispatcher }
assertFailsWith<IllegalStateException> { ContextScope(EmptyCoroutineContext).job }
assertNull(CoroutineScope(EmptyCoroutineContext).dispatcherOrNull)
assertNotNull(CoroutineScope(EmptyCoroutineContext).job)
assertSame(Dispatchers.Unconfined, CoroutineScope(Unconfined).dispatcher)
assertSame(Dispatchers.Unconfined, CoroutineScope(Unconfined).dispatcherOrNull)
}

private fun scopePlusContext(c1: CoroutineContext, c2: CoroutineContext) =
(ContextScope(c1) + c2).coroutineContext
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class CoroutineSchedulerCloseStressTest(private val mode: Mode) : TestBase() {
private val rnd = Random()

private lateinit var closeableDispatcher: ExperimentalCoroutineDispatcher
private lateinit var dispatcher: ExecutorCoroutineDispatcher
private lateinit var coroutineDispatcher: ExecutorCoroutineDispatcher
private var closeIndex = -1

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

private fun launchCoroutines() = runBlocking {
closeableDispatcher = ExperimentalCoroutineDispatcher(N_THREADS)
dispatcher = when (mode) {
coroutineDispatcher = when (mode) {
Mode.CPU -> closeableDispatcher
Mode.CPU_LIMITED -> closeableDispatcher.limited(N_THREADS) as ExecutorCoroutineDispatcher
Mode.BLOCKING -> closeableDispatcher.blocking(N_THREADS) as ExecutorCoroutineDispatcher
}
started.value = 0
finished.value = 0
withContext(dispatcher) {
withContext(coroutineDispatcher) {
launchChild(0, 0)
}
assertEquals(N_COROS, started.value)
Expand Down

0 comments on commit 2272c37

Please sign in to comment.