Coroutine Scope

·

3 min read

  • CoroutineScope interface encapsulates the CoroutineContext.

    • Coroutine builders like launch{}, async{} are extensions on the coroutine scope.
  • Coroutine library provides a function called CoroutineScope for initialization of the CoroutineScope.

  • It is generally not recommended to directly access the coroutineContext property of a CoroutineScope

    • such as coroutineContext[CoroutineDispatcher], except for accessing the Job instance for advanced use cases.
  • CoroutineScope always keep an instance of a Job to manage structured concurrency.

  • coroutineContext and currentCoroutineContext()

    • Both returns the coroutineContext instance.

    • currentCoroutineContext() added as an alias to avoid name clash with CoroutineScope.coroutineContext in a receiver position.

  • CoroutineScope extension functions

    • plus(context: CoroutineContext):

      • To override the existing context element in current scope.
    • ensureActive():

      • Ensures that current scope is active.

      • If the job is no longer active, throws CancellationException.

    • cancel(cause: CancellationException? = null):

      • Cancels the current scope, including its job and all its children with an optional cancellation cause.

        • A cancellation 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.

Example: CoroutineScope usage

suspend fun main() {
    val context = EmptyCoroutineContext + Dispatchers.Default
    val scope = CoroutineScope(context)
    scope.launch {
        println("Coroutine launch builder is Coroutine scope's extension function")
    }.join()
}

CoroutineScope Interface

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

CoroutineScope internal initialization implementation

internal class ContextScope(context: CoroutineContext) : CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)"
}

CoroutineScope Function

@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

CoroutineScope(), MainScope(), GlobalScope object and coroutineScope{}

  • CoroutineScope() function:

    • It bounds to a Job when creates coroutine scope with a specified context.
  • MainScope() function:

    • Runs on main thread as it sets Dispatchers.Main dispatcher internally.

    • It is bound with SupervisorJob.

    • On Android, MainScope() is safer instead GlobalScope object to create a scope.

  • GlobalScope object:

    • Uses EmptyCoroutineContext.

    • It is not bound to any job.

  • coroutineScope{} suspend function:

    • Creates a CoroutineScope and it's a suspend function.

    • Calls the specified suspend block with this scope.

    • This scope inherits its 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.

        • SupervisorJob can be used to handle the bi-directional error propagation.
      • This function returns as soon as the given block and all its children coroutines are completed.

CoroutineScope Function

@Suppress("FunctionName")
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
    ContextScope(if (context[Job] != null) context else context + Job())

Main scope function implementation

@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

GlobalScope object implementation

public object GlobalScope : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

coroutineScope{} suspend function

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)
    }
}

Coroutine Scope Cancellation

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)
}

//OR

public fun CoroutineScope.cancel(message: String, cause: Throwable? = null): Unit =
    cancel(CancellationException(message, cause))

Happy coding... 😊