Skip to content

Commit 833229f

Browse files
committed
Structured concurrency implementation:
* Introducing async, launch, produce, actor and broadcast extensions on CoroutineScope * Deprecate top-level coroutine builders * Introducing currentScope and coroutineScope for manipulation with CoroutineScope interface * Introducing CoroutineScope factories * Introducing extension CoroutineScope.isActive Fixes #410
1 parent 750d468 commit 833229f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+795
-226
lines changed

binary-compatibility-validator/test/PublicApiTest.kt

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import java.util.*
1212
import java.util.jar.*
1313
import kotlin.collections.ArrayList
1414

15+
@Ignore
1516
@RunWith(Parameterized::class)
1617
class PublicApiTest(
1718
private val rootDir: String,

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ public abstract class AbstractCoroutine<in T>(
3636
@Suppress("LeakingThis")
3737
public final override val context: CoroutineContext = parentContext + this
3838
@Deprecated("Replaced with context", replaceWith = ReplaceWith("context"))
39-
public final override val coroutineContext: CoroutineContext get() = context
39+
public override val coroutineContext: CoroutineContext get() = context
40+
41+
override val isActive: Boolean get() = super<JobSupport>.isActive
4042

4143
/**
4244
* Initializes parent job from the `parentContext` of this coroutine that was passed to it during construction.

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

+72-20
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,13 @@ import kotlin.coroutines.experimental.intrinsics.*
1818
* Launches new coroutine without blocking current thread and returns a reference to the coroutine as a [Job].
1919
* The coroutine is cancelled when the resulting job is [cancelled][Job.cancel].
2020
*
21-
* The [context] for the new coroutine can be explicitly specified.
22-
* See [CoroutineDispatcher] for the standard context implementations that are provided by `kotlinx.coroutines`.
23-
* The [coroutineContext](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines.experimental/coroutine-context.html)
24-
* of the parent coroutine may be used,
25-
* in which case the [Job] of the resulting coroutine is a child of the job of the parent coroutine.
26-
* The parent job may be also explicitly specified using [parent] parameter.
27-
*
21+
* Coroutine context is inherited from a [CoroutineScope], additional context elements can be specified with [context] argument.
2822
* If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [DefaultDispatcher] is used.
23+
* The parent job is inherited from a [CoroutineScope] as well, but it can also be overridden
24+
* with corresponding [coroutineContext] element.
2925
*
3026
* By default, the coroutine is immediately scheduled for execution.
31-
* Other options can be specified via `start` parameter. See [CoroutineStart] for details.
27+
* Other start options can be specified via `start` parameter. See [CoroutineStart] for details.
3228
* An optional [start] parameter can be set to [CoroutineStart.LAZY] to start coroutine _lazily_. In this case,
3329
* the coroutine [Job] is created in _new_ state. It can be explicitly started with [start][Job.start] function
3430
* and will be started implicitly on the first invocation of [join][Job.join].
@@ -39,35 +35,68 @@ import kotlin.coroutines.experimental.intrinsics.*
3935
*
4036
* See [newCoroutineContext] for a description of debugging facilities that are available for newly created coroutine.
4137
*
42-
* @param context context of the coroutine. The default value is [DefaultDispatcher].
38+
* @param context additional to [CoroutineScope.coroutineContext] context of the coroutine
4339
* @param start coroutine start option. The default value is [CoroutineStart.DEFAULT].
44-
* @param parent explicitly specifies the parent job, overrides job from the [context] (if any).
4540
* @param onCompletion optional completion handler for the coroutine (see [Job.invokeOnCompletion]).
46-
* @param block the coroutine code.
47-
*/
48-
public fun launch(
49-
context: CoroutineContext = DefaultDispatcher,
41+
* @param block the coroutine code which will be invoked in the context of the provided scope
42+
**/
43+
public fun CoroutineScope.launch(
44+
context: CoroutineContext = EmptyCoroutineContext,
5045
start: CoroutineStart = CoroutineStart.DEFAULT,
51-
parent: Job? = null,
5246
onCompletion: CompletionHandler? = null,
5347
block: suspend CoroutineScope.() -> Unit
5448
): Job {
55-
val newContext = newCoroutineContext(context, parent)
49+
val newContext = newCoroutineContext(context)
5650
val coroutine = if (start.isLazy)
5751
LazyStandaloneCoroutine(newContext, block) else
5852
StandaloneCoroutine(newContext, active = true)
5953
if (onCompletion != null) coroutine.invokeOnCompletion(handler = onCompletion)
6054
coroutine.start(start, coroutine, block)
6155
return coroutine
6256
}
57+
58+
/**
59+
* Launches new coroutine without blocking current thread and returns a reference to the coroutine as a [Job].
60+
* @suppress **Deprecated** Use [CoroutineScope.launch] instead.
61+
*/
62+
@Deprecated(
63+
message = "Standalone coroutine builders are deprecated, use extensions on CoroutineScope instead",
64+
replaceWith = ReplaceWith("GlobalScope.launch(context, start, onCompletion, block)", imports = ["kotlinx.coroutines.experimental.*"])
65+
)
66+
public fun launch(
67+
context: CoroutineContext = DefaultDispatcher,
68+
start: CoroutineStart = CoroutineStart.DEFAULT,
69+
onCompletion: CompletionHandler? = null,
70+
block: suspend CoroutineScope.() -> Unit
71+
): Job =
72+
GlobalScope.launch(context, start, onCompletion, block)
73+
74+
/**
75+
* Launches new coroutine without blocking current thread and returns a reference to the coroutine as a [Job].
76+
* @suppress **Deprecated** Use [CoroutineScope.launch] instead.
77+
*/
78+
@Deprecated(
79+
message = "Standalone coroutine builders are deprecated, use extensions on CoroutineScope instead. This API will be hidden in the next release",
80+
replaceWith = ReplaceWith("GlobalScope.launch(context + parent, start, onCompletion, block)", imports = ["kotlinx.coroutines.experimental.*"])
81+
)
82+
public fun launch(
83+
context: CoroutineContext = DefaultDispatcher,
84+
start: CoroutineStart = CoroutineStart.DEFAULT,
85+
parent: Job? = null, // nullable for binary compatibility
86+
onCompletion: CompletionHandler? = null,
87+
block: suspend CoroutineScope.() -> Unit
88+
): Job =
89+
GlobalScope.launch(context + (parent ?: EmptyCoroutineContext), start, onCompletion, block)
90+
6391
/** @suppress **Deprecated**: Binary compatibility */
6492
@Deprecated(message = "Binary compatibility", level = DeprecationLevel.HIDDEN)
6593
public fun launch(
6694
context: CoroutineContext = DefaultDispatcher,
6795
start: CoroutineStart = CoroutineStart.DEFAULT,
6896
parent: Job? = null,
6997
block: suspend CoroutineScope.() -> Unit
70-
): Job = launch(context, start, parent, block = block)
98+
): Job =
99+
GlobalScope.launch(context + (parent ?: EmptyCoroutineContext), start, block = block)
71100

72101
/** @suppress **Deprecated**: Binary compatibility */
73102
@Deprecated(message = "Binary compatibility", level = DeprecationLevel.HIDDEN)
@@ -84,7 +113,7 @@ public fun launch(
84113
@Deprecated(message = "Use `start = CoroutineStart.XXX` parameter",
85114
replaceWith = ReplaceWith("launch(context, if (start) CoroutineStart.DEFAULT else CoroutineStart.LAZY, block)"))
86115
public fun launch(context: CoroutineContext, start: Boolean, block: suspend CoroutineScope.() -> Unit): Job =
87-
launch(context, if (start) CoroutineStart.DEFAULT else CoroutineStart.LAZY, block = block)
116+
GlobalScope.launch(context, if (start) CoroutineStart.DEFAULT else CoroutineStart.LAZY, block = block)
88117

89118
/**
90119
* Calls the specified suspending block with a given coroutine context, suspends until it completes, and returns
@@ -103,6 +132,19 @@ public fun launch(context: CoroutineContext, start: Boolean, block: suspend Coro
103132
* A value of [CoroutineStart.LAZY] is not supported and produces [IllegalArgumentException].
104133
*/
105134
public suspend fun <T> withContext(
135+
context: CoroutineContext,
136+
start: CoroutineStart = CoroutineStart.DEFAULT,
137+
block: suspend CoroutineScope.() -> T
138+
): T =
139+
// todo: optimize fast-path to work without allocation (when there is a already a coroutine implementing scope)
140+
withContextImpl(context, start) {
141+
currentScope {
142+
block()
143+
}
144+
}
145+
146+
// todo: optimize it to reduce allocations
147+
private suspend fun <T> withContextImpl(
106148
context: CoroutineContext,
107149
start: CoroutineStart = CoroutineStart.DEFAULT,
108150
block: suspend () -> T
@@ -137,6 +179,16 @@ public suspend fun <T> withContext(
137179
completion.getResult()
138180
}
139181

182+
/** @suppress **Deprecated**: Binary compatibility */
183+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Binary compatibility")
184+
@JvmName("withContext")
185+
public suspend fun <T> withContext0(
186+
context: CoroutineContext,
187+
start: CoroutineStart = CoroutineStart.DEFAULT,
188+
block: suspend () -> T
189+
): T =
190+
withContextImpl(context, start, block)
191+
140192
/** @suppress **Deprecated**: Renamed to [withContext]. */
141193
@Deprecated(message = "Renamed to `withContext`", level=DeprecationLevel.WARNING,
142194
replaceWith = ReplaceWith("withContext(context, start, block)"))
@@ -145,12 +197,12 @@ public suspend fun <T> run(
145197
start: CoroutineStart = CoroutineStart.DEFAULT,
146198
block: suspend () -> T
147199
): T =
148-
withContext(context, start, block)
200+
withContextImpl(context, start, block)
149201

150202
/** @suppress **Deprecated** */
151203
@Deprecated(message = "It is here for binary compatibility only", level=DeprecationLevel.HIDDEN)
152204
public suspend fun <T> run(context: CoroutineContext, block: suspend () -> T): T =
153-
withContext(context, start = CoroutineStart.ATOMIC, block = block)
205+
withContextImpl(context, start = CoroutineStart.ATOMIC, block = block)
154206

155207
// --------------- implementation ---------------
156208

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

+173-12
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,51 @@
44

55
package kotlinx.coroutines.experimental
66

7+
import kotlinx.coroutines.experimental.internal.*
78
import kotlin.coroutines.experimental.*
8-
import kotlin.internal.*
99

1010
/**
11-
* Receiver interface for generic coroutine builders, so that the code inside coroutine has a convenient
12-
* and fast access to its own cancellation status via [isActive].
11+
* Defines a scope for new coroutines. Every coroutine builder
12+
* is an extension on [CoroutineScope] and inherits its [coroutineContext][CoroutineScope.coroutineContext]
13+
* to automatically propagate both context elements and cancellation.
14+
*
15+
* [CoroutineScope] should be implemented on entities with well-defined lifecycle that are responsible
16+
* for launching children coroutines. Example of such entity on Android is Activity.
17+
* Usage of this interface may look like this:
18+
*
19+
* ```
20+
* class MyActivity : AppCompatActivity(), CoroutineScope {
21+
*
22+
* override val coroutineContext: CoroutineContext
23+
* get() = job + UI
24+
*
25+
* override fun onCreate(savedInstanceState: Bundle?) {
26+
* super.onCreate(savedInstanceState)
27+
* job = Job()
28+
* }
29+
*
30+
* override fun onDestroy() {
31+
* super.onDestroy()
32+
* job.cancel() // Cancel job on activity destroy. After destroy all children jobs will be cancelled automatically
33+
* }
34+
*
35+
* /*
36+
* * Note how coroutine builders are scoped: if activity is destroyed or any of the launched coroutines
37+
* * in this method throws an exception, then all nested coroutines will be cancelled.
38+
* */
39+
* fun loadDataFromUI() = launch { // <- extension on current activity, launched in CommonPool
40+
* val ioData = async(IO) { // <- extension on launch scope, launched in IO dispatcher
41+
* // long computation
42+
* }
43+
*
44+
* withContext(UI) {
45+
* val data = ioData.await()
46+
* draw(data)
47+
* }
48+
* }
49+
* }
50+
*
51+
* ```
1352
*/
1453
public interface CoroutineScope {
1554
/**
@@ -26,18 +65,140 @@ public interface CoroutineScope {
2665
* [CoroutineScope] is available.
2766
* See [coroutineContext][kotlin.coroutines.experimental.coroutineContext],
2867
* [isActive][kotlinx.coroutines.experimental.isActive] and [Job.isActive].
68+
*
69+
* @suppress **Deprecated**: Deprecated in favor of top-level extension property
2970
*/
71+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Deprecated in favor of top-level extension property")
3072
public val isActive: Boolean
73+
get() = coroutineContext[Job]?.isActive ?: true
3174

3275
/**
33-
* Returns the context of this coroutine.
34-
*
35-
* @suppress: **Deprecated**: Replaced with top-level [kotlin.coroutines.experimental.coroutineContext].
76+
* Returns the context of this scope.
3677
*/
37-
@Deprecated("Replace with top-level coroutineContext",
38-
replaceWith = ReplaceWith("coroutineContext",
39-
imports = ["kotlin.coroutines.experimental.coroutineContext"]))
40-
@LowPriorityInOverloadResolution
41-
@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
4278
public val coroutineContext: CoroutineContext
43-
}
79+
}
80+
81+
/**
82+
* Adds the specified coroutine context to this scope, overriding existing elements in the current
83+
* scope's context with the corresponding keys.
84+
*
85+
* This is a shorthand for `CoroutineScope(thisScope + context)`.
86+
*/
87+
public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =
88+
CoroutineScope(context + context)
89+
90+
/**
91+
* Returns `true` when current [Job] is still active (has not completed and was not cancelled yet).
92+
*
93+
* Check this property in long-running computation loops to support cancellation:
94+
* ```
95+
* while (_isActive) {
96+
* // do some computation
97+
* }
98+
* ```
99+
*
100+
* This property is a shortcut for `coroutineContext.isActive` in the scope when
101+
* [CoroutineScope] is available.
102+
* See [coroutineContext][kotlin.coroutines.experimental.coroutineContext],
103+
* [isActive][kotlinx.coroutines.experimental.isActive] and [Job.isActive].
104+
*/
105+
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
106+
public val CoroutineScope.isActive: Boolean
107+
get() = coroutineContext[Job]?.isActive ?: true
108+
109+
/**
110+
* A global [CoroutineScope] not bound to any job.
111+
*
112+
* Global scope is used to launch top-level coroutines which are operating on the whole application lifetime
113+
* and are not cancelled prematurely.
114+
* Another use of the global scope is [Unconfined] operators, which don't have any job associated with them.
115+
*
116+
* Application code usually should use application-defined [CoroutineScope], using [async] or [launch]
117+
* on the instance of [GlobalScope] is highly discouraged.
118+
*
119+
* Usage of this interface may look like this:
120+
*
121+
* ```
122+
* fun ReceiveChannel<Int>.sqrt(): ReceiveChannel<Double> = GlobalScope.produce(Unconfined) {
123+
* for (number in this) {
124+
* send(Math.sqrt(number))
125+
* }
126+
* }
127+
*
128+
* ```
129+
*/
130+
object GlobalScope : CoroutineScope {
131+
/**
132+
* @suppress **Deprecated**: Deprecated in favor of top-level extension property
133+
*/
134+
@Deprecated(level = DeprecationLevel.HIDDEN, message = "Deprecated in favor of top-level extension property")
135+
override val isActive: Boolean
136+
get() = true
137+
138+
/**
139+
* Returns [EmptyCoroutineContext].
140+
*/
141+
override val coroutineContext: CoroutineContext
142+
get() = EmptyCoroutineContext
143+
}
144+
145+
/**
146+
* Creates new [CoroutineScope] and calls the specified suspend block with this scope.
147+
* The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
148+
* context's [Job].
149+
*
150+
* This methods returns as soon as given block and all launched from within the scope children coroutines are completed.
151+
* Example of the scope usages looks like this:
152+
*
153+
* ```
154+
* suspend fun loadDataForUI() = coroutineScope {
155+
*
156+
* val data = async { // <- extension on current scope
157+
* ... load some UI data ...
158+
* }
159+
*
160+
* withContext(UI) {
161+
* doSomeWork()
162+
* val result = data.await()
163+
* display(result)
164+
* }
165+
* }
166+
* ```
167+
*
168+
* Semantics of the scope in this example:
169+
* 1) `loadDataForUI` returns as soon as data is loaded and UI is updated.
170+
* 2) If `doSomeWork` throws an exception, then `async` task is cancelled and `loadDataForUI` rethrows that exception.
171+
* 3) If outer scope of `loadDataForUI` is cancelled, both started `async` and `withContext` are cancelled.
172+
*
173+
* Method may throw [JobCancellationException] if the current job was cancelled externally
174+
* or may throw the corresponding unhandled [Throwable] if there is any unhandled exception in this scope
175+
* (for example, from a crashed coroutine that was started with [launch] in this scope).
176+
*/
177+
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
178+
// todo: optimize implementation to a single allocated object
179+
val owner = ScopeOwnerCoroutine<R>(coroutineContext)
180+
owner.start(CoroutineStart.UNDISPATCHED, owner, block)
181+
owner.join()
182+
if (owner.isCancelled) {
183+
throw owner.getCancellationException().let { it.cause ?: it }
184+
}
185+
val state = owner.state
186+
if (state is CompletedExceptionally) {
187+
throw state.cause
188+
}
189+
@Suppress("UNCHECKED_CAST")
190+
return state as R
191+
}
192+
193+
/**
194+
* Provides [CoroutineScope] that is already present in the current [coroutineContext] to the given [block].
195+
* Note, this method doesn't wait for all launched children to complete (as opposed to [coroutineContext]).
196+
*/
197+
public suspend inline fun <R> currentScope(block: CoroutineScope.() -> R): R =
198+
CoroutineScope(coroutineContext).block()
199+
200+
/**
201+
* Creates [CoroutineScope] that wraps the given [coroutineContext].
202+
*/
203+
@Suppress("FunctionName")
204+
public fun CoroutineScope(context: CoroutineContext): CoroutineScope = ContextScope(context)

0 commit comments

Comments
 (0)