Skip to content

Discussion: Make ThreadContextElement or its analog available on all platforms #3326

Open
@eymar

Description

@eymar

Currently, ThreadContextElement interface is available only for jvm and it has 2 functions: 1st invoked before a coroutine is resumed, 2nd invoked after that coroutine is suspended.

These functions should never throw an exception:

Thrown exceptions will leave coroutine which context is updated in an undefined state and may crash an application.

public interface ThreadContextElement<S> : CoroutineContext.Element {
    /**
     * This function is invoked before the coroutine in the specified [context] is resumed in the current thread
     * when the context of the coroutine this element.
     */
    public fun updateThreadContext(context: CoroutineContext): S

    /**
     * This function is invoked after the coroutine in the specified [context] is suspended in the current thread
     * if [updateThreadContext] was previously invoked on resume of this coroutine.
     */
    public fun restoreThreadContext(context: CoroutineContext, oldState: S)
}

The need to have it for all targets (jvm, k/js, k/native) appeared in Compose Runtime library. Currently it's used in JVM only, but something similar needed to provide correct implementations of new Compose API for k/js and k/native .

Use Case
Here is a test from compose to showcase an example of desired behaviour:

/**
To have better intuition about this example, it might be useful to  look at Snapshot in Compose. 
Snapshot resembles a version control system with branches and read, write, merge operations. 
This system manages the states of different objects (Compose's state objects). 
Mutations made with one snapshot/branch do not interfere with other snapshots, 
but a Mutable Snapshot can be applied/commited to its parent/root.
Here is a good article: https://dev.to/zachklipp/introduction-to-the-compose-snapshot-system-19cn

*/

/* Example dependencies */

fun Snapshot.takeSnapshot(): Snapshot // "branches" a new Snapshot from a current Snapshot (from Global by default)

// Snapshot.current is a public static property. Its `getter` is accessing a ThreadLocal (Compose has an internal expect/actual ThreadLocal)
val current: Snapshot
    get() = threadSnapshot.get() ?: currentGlobalSnapshot.get() //threadSnapshot is ThreadLocal
    
// Snapshot.asContextElement() in an extension that converts a Snapshot to CoroutineContext.Element:
fun Snapshot.asContextElement(): SnapshotContextElement = SnapshotContextElementImpl(this) //  implements ThreadContextElement and sets/restores a values accessed via Snapshot.current

/* Example */
val snapshotOne = Snapshot.takeSnapshot() // branch from the Global snapshot
val snapshotTwo = Snapshot.takeSnapshot() // branch from the Global snapshot
runBlocking {
    val stopA = Job()
    val jobA = launch(snapshotOne.asContextElement()) { // associate  snapshotOne with this coroutine
        assertSame(snapshotOne, Snapshot.current, "expected snapshotOne, A")
        stopA.join() // suspend, so Snapshot.current will be restored
        assertSame(snapshotOne, Snapshot.current, "expected snapshotOne, B")
    }
    launch(snapshotTwo.asContextElement()) { // associate  snapshotTwo with this coroutine
        assertSame(snapshotTwo, Snapshot.current, "expected snapshotTwo, A")
        stopA.complete()
        jobA.join() // suspend, so Snapshot.current will be restored
        assertSame(snapshotTwo, Snapshot.current, "expected snapshotTwo, B")
    }
}

A coroutine can read/write a snapshot by accessing it via Snapshot.current (even if there is no SnapshotContextElement, Snapshot.current will return a snapshot from either a ThreadLocal or a global snapshot).
SnapshotContextElement lets us have independent snapshots in different coroutines within one thread, so a coroutine can have an associated Snapshot.

ThreadContextElement can be further helpful to perform Snapshot.apply (commit changes) in restoreThreadContext, when a coroutine suspends.

Not existing feature: (asked by Adam Powell)
Let's say we want to commit a Snapshot when coroutine with SnapshotContextElement suspends: so we call Snapshot.apply from ThreadContextElement.restoreThreadContext. The thing is apply can return SnapshotApplyResult.Failure.
In that case, it's desirable to resume a suspended coroutine right away with an exception. It would need to be an arbitrary exception and not a CancellationException, as it should eventually result in failing the job tree.

Question: Can we somehow have an access to a continuation of a suspended coroutine within a CoroutineContext.Element (right after it was suspended)?

Question to conclude the above:

  • Can we have a common way to hook into the coroutine lifecycle? To have a kind of callback when a coroutine resumes and suspends? (like ThreadContextElement in jvm)
  • If we had such hooks, can we optionally manage that coroutine within those hooks? Like resumeWith(error) immediately after suspension in restoreThreadContext ?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions