`run` is optimized with fast-path case and no longer has `CoroutineScope` in its block
diff --git a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Builders.kt b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Builders.kt
index 4aed46f..833824e 100644
--- a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Builders.kt
+++ b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/Builders.kt
@@ -18,6 +18,8 @@
import java.util.concurrent.locks.LockSupport
import kotlin.coroutines.experimental.*
+import kotlin.coroutines.experimental.intrinsics.startCoroutineUninterceptedOrReturn
+import kotlin.coroutines.experimental.intrinsics.suspendCoroutineOrReturn
// --------------- basic coroutine builders ---------------
@@ -58,10 +60,28 @@
* different thread inside the block, and back when it completes.
* The specified [context] is added onto the current coroutine context for the execution of the block.
*/
-public suspend fun <T> run(context: CoroutineContext, block: suspend CoroutineScope.() -> T): T =
- suspendCoroutine { cont ->
- // new don't invoke `newCoroutineContext`, but consider this being the same coroutine in the new context
- InnerCoroutine(cont.context + context, cont).also { block.startCoroutine(it, it) }
+public suspend fun <T> run(context: CoroutineContext, block: suspend () -> T): T =
+ suspendCoroutineOrReturn sc@ { cont ->
+ val oldContext = cont.context
+ // fast path #1 if there is no change in the actual context:
+ if (context === oldContext || context is CoroutineContext.Element && oldContext[context.key] === context)
+ return@sc block.startCoroutineUninterceptedOrReturn(cont)
+ // compute new context
+ val newContext = oldContext + context
+ // fast path #2 if the result is actually the same
+ if (newContext === oldContext)
+ return@sc block.startCoroutineUninterceptedOrReturn(cont)
+ // fast path #3 if the new dispatcher is the same as the old one
+ if (newContext[ContinuationInterceptor] === oldContext[ContinuationInterceptor]) {
+ val newContinuation = RunContinuationDirect(newContext, cont)
+ return@sc block.startCoroutineUninterceptedOrReturn(newContinuation)
+ }
+ // slowest path otherwise -- use new interceptor, sync to its result via a
+ // full-blown instance of CancellableContinuation
+ val newContinuation = RunContinuationCoroutine(newContext, cont)
+ newContinuation.initCancellability()
+ block.startCoroutine(newContinuation)
+ newContinuation.getResult()
}
/**
@@ -111,12 +131,15 @@
}
}
-private class InnerCoroutine<in T>(
+private class RunContinuationDirect<in T>(
override val context: CoroutineContext,
continuation: Continuation<T>
-) : Continuation<T> by continuation, CoroutineScope {
- override val isActive: Boolean = context[Job]?.isActive ?: true
-}
+) : Continuation<T> by continuation
+
+private class RunContinuationCoroutine<in T>(
+ override val parentContext: CoroutineContext,
+ continuation: Continuation<T>
+) : CancellableContinuationImpl<T>(continuation, active = true)
private class BlockingCoroutine<T>(
override val parentContext: CoroutineContext,
diff --git a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CancellableContinuation.kt b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CancellableContinuation.kt
index 0d92c15..569ed0f 100644
--- a/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CancellableContinuation.kt
+++ b/kotlinx-coroutines-core/src/main/kotlin/kotlinx/coroutines/experimental/CancellableContinuation.kt
@@ -179,7 +179,7 @@
}
override fun initCancellability() {
- initParentJob(delegate.context[Job])
+ initParentJob(parentContext[Job])
}
@PublishedApi
diff --git a/kotlinx-coroutines-core/src/test/kotlin/kotlinx/coroutines/experimental/RunTest.kt b/kotlinx-coroutines-core/src/test/kotlin/kotlinx/coroutines/experimental/RunTest.kt
new file mode 100644
index 0000000..5b13391
--- /dev/null
+++ b/kotlinx-coroutines-core/src/test/kotlin/kotlinx/coroutines/experimental/RunTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2016-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package kotlinx.coroutines.experimental
+
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.core.IsEqual
+import org.junit.Test
+
+class RunTest : TestBase() {
+ @Test
+ fun testSameContextNoSuspend() = runBlocking<Unit> {
+ expect(1)
+ launch(context) { // make sure there is not early dispatch here
+ finish(5)
+ }
+ expect(2)
+ val result = run(context) { // same context!
+ expect(3) // still here
+ "OK"
+ }
+ assertThat(result, IsEqual("OK"))
+ expect(4)
+ }
+
+ @Test
+ fun testSameContextWithSuspend() = runBlocking<Unit> {
+ expect(1)
+ launch(context) { // make sure there is not early dispatch here
+ expect(4)
+ }
+ expect(2)
+ val result = run(context) { // same context!
+ expect(3) // still here
+ yield() // now yields to launch!
+ expect(5)
+ "OK"
+ }
+ assertThat(result, IsEqual("OK"))
+ finish(6)
+ }
+
+ @Test
+ fun testCancelWithJobNoSuspend() = runBlocking<Unit> {
+ expect(1)
+ launch(context) { // make sure there is not early dispatch to here
+ finish(6)
+ }
+ expect(2)
+ val job = Job()
+ val result = run(context + job) { // same context + new job
+ expect(3) // still here
+ job.cancel() // cancel out job!
+ try {
+ yield() // shall throw CancellationException
+ expectUnreached()
+ } catch (e: CancellationException) {
+ expect(4)
+ }
+ "OK"
+ }
+ assertThat(result, IsEqual("OK"))
+ expect(5)
+ }
+
+ @Test
+ fun testCancelWithJobWithSuspend() = runBlocking<Unit> {
+ expect(1)
+ launch(context) { // make sure there is not early dispatch to here
+ expect(4)
+ }
+ expect(2)
+ val job = Job()
+ val result = run(context + job) { // same context + new job
+ expect(3) // still here
+ yield() // now yields to launch!
+ expect(5)
+ job.cancel() // cancel out job!
+ try {
+ yield() // shall throw CancellationExpcetion
+ expectUnreached()
+ } catch (e: CancellationException) {
+ expect(6)
+ }
+ "OK"
+ }
+ assertThat(result, IsEqual("OK"))
+ finish(7)
+ }
+
+ @Test
+ fun testCommonPoolNoSuspend() = runBlocking<Unit> {
+ expect(1)
+ val result = run(CommonPool) {
+ expect(2)
+ "OK"
+ }
+ assertThat(result, IsEqual("OK"))
+ finish(3)
+ }
+
+ @Test
+ fun testCommonPoolWithSuspend() = runBlocking<Unit> {
+ expect(1)
+ val result = run(CommonPool) {
+ expect(2)
+ delay(100)
+ expect(3)
+ "OK"
+ }
+ assertThat(result, IsEqual("OK"))
+ finish(4)
+ }
+}
\ No newline at end of file