-
Notifications
You must be signed in to change notification settings - Fork 1.8k
/
CoroutineScope.kt
329 lines (315 loc) · 13.6 KB
/
CoroutineScope.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
/*
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/
@file:OptIn(ExperimentalContracts::class)
package kotlinx.coroutines
import kotlinx.coroutines.internal.*
import kotlinx.coroutines.intrinsics.*
import kotlin.contracts.*
import kotlin.coroutines.*
import kotlin.coroutines.intrinsics.*
/**
* Defines a scope for new coroutines. Every **coroutine builder** (like [launch], [async], etc)
* is an extension on [CoroutineScope] and inherits its [coroutineContext][CoroutineScope.coroutineContext]
* to automatically propagate all its elements and cancellation.
*
* The best ways to obtain a standalone instance of the scope are [CoroutineScope()] and [MainScope()] factory functions,
* taking care to cancel these coroutine scopes when they are no longer needed (see section on custom usage below for
* explanation and example).
*
* Additional context elements can be appended to the scope using the [plus][CoroutineScope.plus] operator.
*
* ### Convention for structured concurrency
*
* Manual implementation of this interface is not recommended, implementation by delegation should be preferred instead.
* By convention, the [context of a scope][CoroutineScope.coroutineContext] should contain an instance of a
* [job][Job] to enforce the discipline of **structured concurrency** with propagation of cancellation.
*
* Every coroutine builder (like [launch], [async], etc)
* and every scoping function (like [coroutineScope], [withContext], etc) provides _its own_ scope
* with its own [Job] instance into the inner block of code it runs.
* By convention, they all wait for all the coroutines inside their block to complete before completing themselves,
* thus enforcing the structured concurrency. See [Job] documentation for more details.
*
* ### Android usage
*
* Android has first-party support for coroutine scope in all entities with the lifecycle.
* See [the corresponding documentation](https://developer.android.com/topic/libraries/architecture/coroutines#lifecyclescope).
*
* ### Custom usage
*
* `CoroutineScope` should be declared as a property on entities with a well-defined lifecycle that are
* responsible for launching children coroutines. The corresponding instance of `CoroutineScope` shall be created
* with either `CoroutineScope()` or `MainScope()` functions. The difference between them is only in the
* [CoroutineDispatcher]:
*
* * `CoroutineScope()` uses [Dispatchers.Default] for its coroutines.
* * `MainScope()` uses [Dispatchers.Main] for its coroutines.
*
* **The key part of custom usage of `CustomScope` is cancelling it at the end of the lifecycle.**
* The [CoroutineScope.cancel] extension function shall be used when the entity that was launching coroutines
* is no longer needed. It cancels all the coroutines that might still be running on behalf of it.
*
* For example:
*
* ```
* class MyUIClass {
* val scope = MainScope() // the scope of MyUIClass, uses Dispatchers.Main
*
* fun destroy() { // destroys an instance of MyUIClass
* scope.cancel() // cancels all coroutines launched in this scope
* // ... do the rest of cleanup here ...
* }
*
* /*
* * Note: if this instance is destroyed or any of the launched coroutines
* * in this method throws an exception, then all nested coroutines are cancelled.
* */
* fun showSomeData() = scope.launch { // launched in the main thread
* // ... here we can use suspending functions or coroutine builders with other dispatchers
* draw(data) // draw in the main thread
* }
* }
* ```
*/
public interface CoroutineScope {
/**
* The context of this scope.
* Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
* Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
*
* By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
*/
public val coroutineContext: CoroutineContext
}
/**
* Adds the specified coroutine context to this scope, overriding existing elements in the current
* scope's context with the corresponding keys.
*
* This is a shorthand for `CoroutineScope(thisScope + context)`.
*/
public operator fun CoroutineScope.plus(context: CoroutineContext): CoroutineScope =
ContextScope(coroutineContext + context)
/**
* Creates the main [CoroutineScope] for UI components.
*
* Example of use:
* ```
* class MyAndroidActivity {
* private val scope = MainScope()
*
* override fun onDestroy() {
* super.onDestroy()
* scope.cancel()
* }
* }
* ```
*
* The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements.
* If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator:
* `val scope = MainScope() + CoroutineName("MyActivity")`.
*/
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
/**
* Returns `true` when the current [Job] is still active (has not completed and was not cancelled yet).
*
* Check this property in long-running computation loops to support cancellation:
* ```
* while (isActive) {
* // do some computation
* }
* ```
*
* This property is a shortcut for `coroutineContext.isActive` in the scope when
* [CoroutineScope] is available.
* 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
/**
* A global [CoroutineScope] not bound to any job.
* Global scope is used to launch top-level coroutines which are operating on the whole application lifetime
* and are not cancelled prematurely.
*
* Active coroutines launched in `GlobalScope` do not keep the process alive. They are like daemon threads.
*
* This is a **delicate** API. It is easy to accidentally create resource or memory leaks when
* `GlobalScope` is used. A coroutine launched in `GlobalScope` is not subject to the principle of structured
* concurrency, so if it hangs or gets delayed due to a problem (e.g. due to a slow network), it will stay working
* and consuming resources. For example, consider the following code:
*
* ```
* fun loadConfiguration() {
* GlobalScope.launch {
* val config = fetchConfigFromServer() // network request
* updateConfiguration(config)
* }
* }
* ```
*
* A call to `loadConfiguration` creates a coroutine in the `GlobalScope` that works in background without any
* provision to cancel it or to wait for its completion. If a network is slow, it keeps waiting in background,
* consuming resources. Repeated calls to `loadConfiguration` will consume more and more resources.
*
* ### Possible replacements
*
* In many cases uses of `GlobalScope` should be removed, marking the containing operation with `suspend`, for example:
*
* ```
* suspend fun loadConfiguration() {
* val config = fetchConfigFromServer() // network request
* updateConfiguration(config)
* }
* ```
*
* In cases when `GlobalScope.launch` was used to launch multiple concurrent operations, the corresponding
* operations shall be grouped with [coroutineScope] instead:
*
* ```
* // concurrently load configuration and data
* suspend fun loadConfigurationAndData() {
* coroutinesScope {
* launch { loadConfiguration() }
* launch { loadData() }
* }
* }
* ```
*
* In top-level code, when launching a concurrent operation from a non-suspending context, an appropriately
* confined instance of [CoroutineScope] shall be used instead of a `GlobalScope`. See docs on [CoroutineScope] for
* details.
*
* ### GlobalScope vs custom scope
*
* Do not replace `GlobalScope.launch { ... }` with `CoroutineScope().launch { ... }` constructor function call.
* The latter has the same pitfalls as `GlobalScope`. See [CoroutineScope] documentation on the intended usage of
* `CoroutineScope()` constructor function.
*
* ### Legitimate use-cases
*
* There are limited circumstances under which `GlobalScope` can be legitimately and safely used, such as top-level background
* processes that must stay active for the whole duration of the application's lifetime. Because of that, any use
* of `GlobalScope` requires an explicit opt-in with `@OptIn(DelicateCoroutinesApi::class)`, like this:
*
* ```
* // A global coroutine to log statistics every second, must be always active
* @OptIn(DelicateCoroutinesApi::class)
* val globalScopeReporter = GlobalScope.launch {
* while (true) {
* delay(1000)
* logStatistics()
* }
* }
* ```
*/
@DelicateCoroutinesApi
public object GlobalScope : CoroutineScope {
/**
* Returns [EmptyCoroutineContext].
*/
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
/**
* Creates a [CoroutineScope] and calls the specified suspend block with this scope.
* The provided scope inherits its [coroutineContext][CoroutineScope.coroutineContext] from the outer scope, but overrides
* the context's [Job].
*
* This function is designed for _parallel decomposition_ of work. When any child coroutine in this scope fails,
* this scope fails and all the rest of the children are cancelled (for a different behavior see [supervisorScope]).
* This function returns as soon as the given block and all its children coroutines are completed.
* A usage example of a scope looks like this:
*
* ```
* suspend fun showSomeData() = coroutineScope {
* val data = async(Dispatchers.IO) { // <- extension on current scope
* ... load some UI data for the Main thread ...
* }
*
* withContext(Dispatchers.Main) {
* doSomeWork()
* val result = data.await()
* display(result)
* }
* }
* ```
*
* The scope in this example has the following semantics:
* 1) `showSomeData` returns as soon as the data is loaded and displayed in the UI.
* 2) If `doSomeWork` throws an exception, then the `async` task is cancelled and `showSomeData` rethrows that exception.
* 3) If the outer scope of `showSomeData` is cancelled, both started `async` and `withContext` blocks are cancelled.
* 4) If the `async` block fails, `withContext` will be cancelled.
*
* The method may throw a [CancellationException] if the current job was cancelled externally
* or may throw a corresponding unhandled [Throwable] if there is any unhandled exception in this scope
* (for example, from a crashed coroutine that was started with [launch][CoroutineScope.launch] in this scope).
*/
public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = ScopeCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
/**
* Creates a [CoroutineScope] that wraps the given coroutine [context].
*
* If the given [context] does not contain a [Job] element, then a default `Job()` is created.
* This way, cancellation or failure of any child coroutine in this scope cancels all the other children,
* just like inside [coroutineScope] block.
*/
@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())
/**
* Cancels this scope, including its job and all its children with an optional cancellation [cause].
* A cause can be used to specify an error message or to provide other details on
* a cancellation reason for debugging purposes.
* Throws [IllegalStateException] if the scope does not have a job in it.
*/
public fun CoroutineScope.cancel(cause: CancellationException? = null) {
val job = coroutineContext[Job] ?: error("Scope cannot be cancelled because it does not have a job: $this")
job.cancel(cause)
}
/**
* Cancels this scope, including its job and all its children with a specified diagnostic error [message].
* A [cause] can be specified to provide additional details on a cancellation reason for debugging purposes.
* Throws [IllegalStateException] if the scope does not have a job in it.
*/
public fun CoroutineScope.cancel(message: String, cause: Throwable? = null): Unit = cancel(CancellationException(message, cause))
/**
* Ensures that current scope is [active][CoroutineScope.isActive].
*
* If the job is no longer active, throws [CancellationException].
* If the job was cancelled, thrown exception contains the original cancellation cause.
* This function does not do anything if there is no [Job] in the scope's [coroutineContext][CoroutineScope.coroutineContext].
*
* This method is a drop-in replacement for the following code, but with more precise exception:
* ```
* if (!isActive) {
* throw CancellationException()
* }
* ```
*
* @see CoroutineContext.ensureActive
*/
public fun CoroutineScope.ensureActive(): Unit = coroutineContext.ensureActive()
/**
* Returns the current [CoroutineContext] retrieved by using [kotlin.coroutines.coroutineContext].
* This function is an alias to avoid name clash with [CoroutineScope.coroutineContext] in a receiver position:
*
* ```
* launch { // this: CoroutineScope
* val flow = flow<Unit> {
* coroutineContext // Resolves into the context of outer launch, which is incorrect, see KT-38033
* currentCoroutineContext() // Retrieves actual context where the flow is collected
* }
* }
* ```
*/
public suspend inline fun currentCoroutineContext(): CoroutineContext = coroutineContext