CoroutineContext is the core concept in Kotlin coroutines. What does it do? What elements does it consist of? Why is it designed this way? This article attempts to analyze the source code to answer these questions.

Is a Indexed set both a SET and a map?

CoroutineContext is defined as follows:

/** * Persistent context for the coroutine. It is an indexed set of [Element] instances. * An indexed set is a mix between a set and a map. * Every element in this set has a unique [Key]. */
public interface CoroutineContext {... }Copy the code

CoroutineContext is simply CoroutineContext.

From the perspective of annotations, a context is a collection of elements, which is called a indexed Set. It is a structure between a set and a map. Set means that the elements are unique, and map means that each element has a key.

public interface CoroutineContext {
    // Element is also a context
    public interface Element : CoroutineContext { ... }
}
Copy the code

It turns out that an Element is also a context, so a coroutine context is a collection of contexts (and itself). Let’s call a set of contexts inside a coroutine context a subcontext.

How does the context guarantee the uniqueness of each subcontext?

public interface CoroutineContext {
    public interface Key<E : Element>
}
Copy the code

The context assigns each subcontext a Key, which is an interface with type information. This interface is usually implemented as companion Object:

// Subcontext: Job
public interface Job : CoroutineContext.Element {
    // Static Key for Job
    public companion objectKey : CoroutineContext.Key<Job> { ... }}// Subcontext: interceptor
public interface ContinuationInterceptor : CoroutineContext.Element {
    // The static Key of the interceptor
    companion object Key : CoroutineContext.Key<ContinuationInterceptor>
}

Subcontext: coroutine name
public data class CoroutineName( val name: String ) : AbstractCoroutineContextElement(CoroutineName) {
    // The static Key of the coroutine name
    public companion object Key : CoroutineContext.Key<CoroutineName>
}

// Subcontext: exception handler
public interface CoroutineExceptionHandler : CoroutineContext.Element {
    // The exception handler's static Key
    public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>
}
Copy the code

A static variable inside a class is meant to be shared by all instances of the class. That is, a globally unique instance of a Key can be used for multiple instances of the subcontext. In a map-like structure, however, each key must be unique, because if you put the same key twice, the new value replaces the old one. In this way, the uniqueness of the key this guarantees that all subcontext instances within the context are unique. That’s what a Indexed Set is all about.

Make a phased summary:

  1. A coroutine context is a collection of elements, and a single element is itself a context, so the definition of a coroutine context is recursive and self-contained (it contains several of itself).

  2. Coroutine context This set is a bit like a set structure in that its elements are unique and do not repeat. To do this, each element has a static key instance, forming a set of key-value pairs, which makes it a bit like a map structure again. This structure between a set and a map is called a indexed Set

Gets the element from a Indexed set

CoroutineContext is a collection of elements. The method for fetching elements is defined as follows:

public interface CoroutineContext {
    // Look for elements in the context based on key
    public operator fun <E : Element> get(key: Key<E>): E?
}
Copy the code

The get() method takes a Key and returns an Element. CoroutineContext subclass Element has an implementation of get() :

public interface CoroutineContext {
    / / element
    public interface Element : CoroutineContext {
        // The key of the element
        public val key: Key<*>
        public override operator fun <E : Element> get(key: Key<E>): E? =
            // Return the current element if the given key is the same as the element's own key, otherwise return null
            if (this.key == key) this as E else null}}Copy the code

A coroutine context is a set of elements, and an element is a context, so an element is a set of elements (explaining the definition of recursion is a bit of a tongue twister). But there’s something special about this set of elements, it only contains one element, which is itself. This can also be seen in the implementation of the element.get () method: When retrieving an Element from Element’s collection of elements, it either returns itself or null.

Coroutine contexts also have an implementation class called CombinedContext, which has the following get() implementation:

// Blend context (garlic)
internal class CombinedContext(
    // Upper left below
    private val left: CoroutineContext,
    / / right elements
    private val element: Element
) : CoroutineContext, Serializable {
    // Look for elements in the context based on key
    override fun <E : Element> get(key: Key<E>): E? {
        var cur = this
        while (true) {
            // If the input key is the same as the key of the right element, return the right element (with a piece of garlic removed)cur.element[key]? .let {return it }
            // If the right element does not match, the search continues to the left
            val next = cur.left
            // Start recursion to the left if there is a mixed context on the top left (still a garlic after peeling one, continue peeling)
            if (next is CombinedContext) {
                cur = next
            } 
            // If the upper left context is not mixed, the recursion ends
            else {
                return next[key]
            }
        }
    }
}
Copy the code

Combinedcontext.get () implements a recursion-like effect with the while loop. The definition of a CombinedContext is itself recursive. It consists of two members: left and Element. The left is a coroutine context, and if the left instance is another CombinedContext, a self-contained recursion occurs. A. left B. element C. body D. skin When I peeled off a piece of garlic, I found that it was still a garlic, but smaller.

The combinedContext.get () algorithm is like “finding the specified piece of skin in a garlic tree.” Every time you peel a piece, you check to see if it’s the one you want, and if it’s not, you peel another, and so on, recursively, until you hit the specified piece or the garlic is empty.

The CombinedContext is still eccentric, that is, the last piece of garlic is not in the center, but is on the far left (when the left type is no longer CombinedContext), but the garlic is traversed from the far right to the left, which gives each piece of garlic a different priority. The earlier it is traversed, The higher the priority.

Make a phased summary:

A CombinedContext is a concrete implementation of a coroutine context, which, like a coroutine context, contains a set of elements organized into a self-contained structure called “eccentric garlic.” Eccentric garlic is also a concrete implementation of indexed set, that is, it guarantees the uniqueness of the elements in the set with unique keys corresponding to unique values. However, different from the “flat” structure such as SET and map, the elements in eccentric garlic are naturally hierarchical. The traversal of garlic structure is carried out from the outer layer inward (from right to left), and the elements traversed first naturally have higher priority.

Append elements to a indexed set

Select element (s) from element (s);

public interface CoroutineContext {
    // Overloaded operators
    public operator fun plus(context: CoroutineContext): CoroutineContext =
    // If the append context is empty (= nothing appended), return the current context (high performance return)
    if (context === EmptyCoroutineContext) this else 
        // Start with the current context
        context.fold(this) { acc, element -> } //
}
Copy the code

CoroutineContext overloads the plus operator with the operator reservation, redefining the semantics of the operator. Kotlin has predefined mappings between function names and operators, called conventions. So this is the plus() and + convention right now. When two instances of CoroutineContext are connected by +, it is equivalent to calling the plus() method, which is intended to increase code readability.

The return value of plus() is CoroutineContext, which makes chained calls like C1 + C2 + c3 convenient.

EmptyCoroutineContext is a special context that contains no elements, as evidenced by the implementation of its get() method:

// Empty coroutine context
public object EmptyCoroutineContext : CoroutineContext, Serializable {
    // Return an empty element
    public override fun <E : Element> get(key: Key<E>): E? = null. }Copy the code

Coroutinecontext.fold (), called in plus(), is the interface that adds elements in the CoroutineContext:

public interface CoroutineContext {
    public fun <R> fold(initial: R, operation: (R.Element) - >R): R
}
Copy the code

Fold () requires the input of an accumulation initial value and the accumulation algorithm operation. Let’s first look at the summation algorithm defined in the plus() method:

public interface CoroutineContext {
    public operator fun plus(context: CoroutineContext): CoroutineContext =
    if (context === EmptyCoroutineContext) this else 
        // Start with the current context
        context.fold(this) { acc, element ->
            // Pull the appended element out to reposition it
            val removed = acc.minusKey(element.key)
            // If the collection contains only append elements, return directly without relocation
            if (removed === EmptyCoroutineContext) element else {
                // Obtain the Interceptor from the set of elements
                val interceptor = removed[ContinuationInterceptor]
                // If the set of elements does not include Interceptor, append the element as the outermost layer
                if (interceptor == null) CombinedContext(removed, element) else {
                    // If the set of elements contains Interceptor, pull it out so that it can be relocated
                    val left = removed.minusKey(ContinuationInterceptor)
                    // The set of elements contains only Interceptor and append elements
                    if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
                        // Use the Interceptor as the outermost skin, and append elements as the second
                        CombinedContext(CombinedContext(left, element), interceptor)
                }
            }
        }
}
Copy the code

The summing algorithm takes two input parameters, one representing the current summing value acc and the other representing the newly appended element element. The above algorithm can be summarized as follows: “When you append elements to a coroutine context, all elements are always relocated. Here’s how it’s located: Place the Interceptor and the newly appending element in the outermost and second outer layers of the eccentric garlic, in turn.”

minusKey()

MinusKey () is also an interface to the coroutine context:

public interface CoroutineContext {
    public fun minusKey(key: Key< * >): CoroutineContext
}
Copy the code

MinusKey () returns a coroutine context whose set of elements is stripped of the key element. Element implements this interface as follows:

public interface Element : CoroutineContext {
    public override fun minusKey(key: Key< * >): CoroutineContext =
        if (this.key == key) EmptyCoroutineContext else this
}
Copy the code

Because Element contains only one Element, an empty context is returned if the Element to be removed is itself, or itself otherwise.

CombineContext implements minusKey() as follows:

internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {
    public override fun minusKey(key: Key< * >): CoroutineContext {
    // 1. If the outermost layer is the element to be removed, return the upper leftelement[key]? .let {return left }
    // 2. Remove the corresponding element from the left context
    val newLeft = left.minusKey(key)
    return when {
        // 2.1 The left context also does not contain the corresponding element
        newLeft === left -> this
        // 2.2 If the left context contains no elements except the corresponding element, return the right element
        newLeft === EmptyCoroutineContext -> element
        // 2.3 Combine the top left and right elements with the corresponding elements removed into a new mixed context
        else -> CombinedContext(newLeft, element)
    }
}
Copy the code

It can be summarized as: find the corresponding garlic skins in the eccentric garlic structure, and remove it, and then recombine all the remaining garlic skins in the original order into the eccentric garlic structure.

Element.fold()

After analyzing the accumulation algorithm, look at Element’s implementation of fold() :

public interface CoroutineContext {
    public interface Element : CoroutineContext {
        public override fun <R> fold(initial: R, operation: (R.Element) - >R): R =
            operation(initial, this)}Copy the code

Element appends itself to this method. In combination with the above accumulation algorithm, Element accumulation can be understood as follows: “Element always considers itself as the Element to be appended. that is, Element always appears on the outermost layer of the eccentric garlic.”

Here’s an example:

val e1 = Element()
val e2 = Element()
val context = e1 + e2
Copy the code

What structure is the context in the above code? The reasoning is as follows:

  • E1 plus e2 is the same thing ase2.fold(e1)
  • Since E2 is of type Element, call element.fold (), which is equivalent tooperation(e1, e2)
  • Operation is just the sum of the above, the sum of the above, and then you get context = CombinedContext(E1, e2).

Here’s a more complicated example:

val e1 = Element()
val e2 = Element()
val e3 = Element()
val c = CombinedContext(e1, e2)
val context = c + e3
Copy the code

What structure is the context in the above code? The reasoning is as follows:

  • C plus e3 is the same thing ase3.fold(c)
  • Since e3 is of type Element, call element.fold (), which is equivalent tooperation(c, e3)
  • Operation is the sum of the above, the sum of the above, and the final result is context = CombinedContext(c, e2).
  • CombinedContext(CombinedContext(E1, e2), E3)

Make a phased summary:

Adding two coroutine contexts means combining their elements to form a new, larger eccentric garlic. If the addend is of type Element, that is, it contains only one Element, then that Element is always appended to the outermost layer of the eccentric garlic.

CombinedContext.fold()

Let’s look at the CombinedContext implementation of fold() :

internal class CombinedContext(
    private val left: CoroutineContext,
    private val element: Element
) : CoroutineContext, Serializable {
    public override fun <R> fold(initial: R, operation: (R.Element) - >R): R =
        operation(left.fold(initial, operation), element)
}
Copy the code

This is much more complicated than Element, because there is recursion.

Here’s another example:

val e1 = Element()
val e2 = Element()
val e3 = Element()
val c = CombinedContext(e1, e2)
val context = e3 + c // This is almost the same as the previous example, except that the addend and the addend are changed
Copy the code

What structure is the context in the above code? The reasoning is as follows:

  • E3 plus c is the same thing asc.fold(e3)
  • Since C is of type CombinedContext, we call CombinedContext.fold(), which is equivalent tooperation(e1.fold(e3), e2)
  • Among theme1.fold(e3)Is equivalent tooperation(e3, e1)Which has the value CombinedContext(e3, e1).
  • I plug in the result of step 3 into step 2, and I get context = CombinedContext(CombinedContext(e3, e1), e2).

One more stage summary:

Adding two coroutine contexts means combining their elements to form a new, larger eccentric garlic. If the addend is of the CombinedContext type, that is, the addend contains a left body and a right skin, then the skin stays where it was, and the garlic merges with the addend to form the new eccentric garlic structure.

conclusion

This article introduces the CoroutineContext data structure, which contains the following characteristics:

  1. A coroutine context is a collection of elements, and a single element is itself a context, so the definition of a coroutine context is recursive and self-contained (it contains several of itself).
  2. Coroutine context This set is a bit like a set structure in that its elements are unique and do not repeat. To do this, each element has a static key instance, forming a set of key-value pairs, which makes it a bit like a map structure again. This structure between a set and a map is calledindexed set
  3. CombinedContextIs a concrete implementation of a coroutine context, which, like a coroutine context, contains a set of elements that are organized into“Eccentric garlic”This self-contained structure. Eccentric garlic is also a concrete implementation of indexed set, that is, it guarantees the uniqueness of the elements in the set with unique keys corresponding to unique values. However, different from the “flat” structure such as SET and map, the elements in eccentric garlic are naturally hierarchical. The traversal of garlic structure is carried out from the outer layer inward (from right to left), and the elements traversed first naturally have higher priority.
  4. Adding two coroutine contexts means combining their elements to form a new, larger eccentric garlic. If the addend is of type Element, that is, it contains only one Element, then that Element is always appended to the outermost layer of the eccentric garlic. If the addend is of the CombinedContext type, that is, the addend contains a left body and a right skin, then the skin stays where it was, and the garlic merges with the addend to form the new eccentric garlic structure.

The next one will continue to examine the benefits of this structure in implementing the coroutine feature. Please follow me for update notifications

Recommended reading

  • Kotlin base | entrusted and its application
  • Kotlin basic grammar | refused to noise
  • Kotlin advanced | not variant, covariant and inverter
  • Kotlin combat | after a year, with Kotlin refactoring a custom controls
  • Kotlin combat | kill shape with syntactic sugar XML file
  • Kotlin base | literal-minded Kotlin set operations
  • Kotlin source | magic weapon to reduce the complexity of code
  • Why Kotlin coroutines | CoroutineContext designed indexed set? (a)
  • Kotlin advanced | the use of asynchronous data stream Flow scenarios