Description
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
?