Dentro de CoroutineContext: Job, dispatcher y llevar un MDC
Un CoroutineContext es un conjunto inmutable indexado por tipo, `+` es unión con prioridad a la derecha, y todo lo confuso sobre los scopes sale de esos dos hechos.
Explorar la explicación completa 8 ideas
Un `CoroutineContext` es un conjunto inmutable e indexado de `Element`s, cada uno identificado por una `Key`. `Dispatchers.IO`, un `Job`, un `CoroutineName`, un `CoroutineExceptionHandler` son todos elementos, y `context[Key]` busca uno por tipo. El operador `+` es unión de conjuntos donde gana el lado derecho ante colisión de clave: `Dispatchers.IO + CoroutineName("import")` es un conjunto de dos elementos, y `ctx + Dispatchers.Default` reemplaza el dispatcher que tuviera `ctx`. Una vez que tenés esa imagen, la API deja de parecer magia y pasa a parecer un `Map<KClass, Element>` con un operador.
El contexto lo **heredan los hijos**. `launch { }` toma el contexto del scope actual, aplica los overrides que le pasaste y — clave — instala un **`Job` nuevo** que es hijo del actual. Esa única sustitución es la concurrencia estructurada: el padre conoce a sus hijos porque ellos tienen su Job como padre, la cancelación recorre el árbol hacia abajo, y `coroutineScope` puede esperarlos a todos sin que registres nada. Todo lo demás del contexto se hereda sin cambios.
El `Job` es la pieza que vale conocer en detalle. Tiene un ciclo de vida — *nuevo → activo → completando → completado*, con *cancelando → cancelado* ramificando — expuesto como `isActive`, `isCompleted` e `isCancelled` (ojo: un Job cancelado reporta `isCompleted == true` una vez que terminó de desenrollarse, algo que sorprende a quien escribe chequeos de estado). `job.children` recorre el árbol. `job.invokeOnCompletion { cause -> … }` es un callback no suspendible en todos los caminos terminales, lo que lo hace mejor lugar para la contabilidad que un `finally` que tiene que pelear con la cancelación. Y `SupervisorJob` difiere en exactamente un comportamiento: el fallo de un hijo no propaga hacia arriba.
El dispatcher es un `ContinuationInterceptor`, que es un elemento de contexto como cualquier otro — intercepta cada reanudación y decide qué hilo la corre. `Dispatchers.Default` es un pool dimensionado según la cantidad de CPUs, para cómputo. `Dispatchers.IO` es un pool elástico mucho más grande que comparte hilos con `Default`, para llamadas bloqueantes. `Dispatchers.Unconfined` se reanuda en el hilo que completó la suspensión anterior y es para tests y casos borde, no para producción. `limitedParallelism(n)` recorta una vista de un dispatcher con un tope duro de concurrencia, que es cómo le das a un subsistema una porción de `IO` sin darle todo `IO`.
`withContext(ctx) { … }` es la herramienta para un override *temporal*. Suspende, corre el bloque con el contexto combinado y devuelve el valor del bloque — así que `withContext(Dispatchers.IO) { jdbcQuery() }` mueve una llamada bloqueante fuera del event loop y vuelve enseguida. No es un builder de corrutinas al estilo `launch`/`async`: no bifurca, y devuelve un valor en vez de un `Job`. Recurrir a `launch` donde querías `withContext` es cómo el código secuencial se vuelve fire-and-forget por accidente.
`CoroutineScope` no es más que un contenedor de un contexto — literalmente una interfaz con una propiedad `coroutineContext`. `CoroutineScope(SupervisorJob() + Dispatchers.Default + handler)` construye uno; `viewModelScope` y `lifecycleScope` son scopes prearmados atados a un ciclo de vida. Por eso la guía «no uses `GlobalScope`» no es estilística: `GlobalScope` no tiene un `Job` que vos poseas, así que nada cancela a sus hijos y cada corrutina lanzada ahí sobrevive a lo que la arrancó.
Lo que hace tropezar al código de servidor es el estado thread-local. Una corrutina puede reanudarse en un hilo distinto del que suspendió, así que un `MDC` de SLF4J, un `SecurityContext` de Spring o cualquier `ThreadLocal` seteado antes de una suspensión simplemente no está después. `ThreadContextElement` es el mecanismo que lo arregla: es un elemento de contexto con hooks `updateThreadContext`/`restoreThreadContext` que el runtime llama alrededor de cada reanudación, así el thread-local se reinstala donde sea que caiga la corrutina. `kotlinx-coroutines-slf4j` trae `MDCContext` construido exactamente así, y `withContext(MDCContext()) { … }` es cómo un correlation id sobrevive a un `await`.
Dos hábitos hacen que el contexto trabaje a favor tuyo. **Nombrá tus corrutinas**: `CoroutineName("order-import")` no cuesta nada, aparece en los stack traces y en los nombres de hilo en modo debug, y convierte un cuelgue anónimo en uno etiquetado. **Inyectá tus dispatchers** en vez de fijarlos: una clase que recibe un parámetro `CoroutineDispatcher` se puede testear con `StandardTestDispatcher` y su reloj virtual, mientras que una que llama directamente a `Dispatchers.IO` no — el test va a dormir de verdad, competir de verdad y ser inestable de verdad.
Aprendamos juntos.
Pregúntame sobre este curso o por dónde empezar.
import kotlinx.coroutines.*import kotlin.coroutines.ContinuationInterceptorfun main() = runBlocking {val base = Dispatchers.Default + CoroutineName("import")println("name = " + base[CoroutineName])println("dispatcher = " + base[ContinuationInterceptor])// Right-hand side wins on a key collision.val overridden = base + CoroutineName("export")println("overridden = " + overridden[CoroutineName])withContext(base) {val parentJob = coroutineContext[Job]launch(CoroutineName("child")) {val childJob = coroutineContext[Job]println("child name = " + coroutineContext[CoroutineName])// Inherited unchanged...println("same disp. = " + (coroutineContext[ContinuationInterceptor] === base[ContinuationInterceptor]))// ...except the Job, which is a fresh CHILD of the parent's.println("same job = " + (childJob === parentJob))println("is a child = " + (parentJob?.children?.contains(childJob) ?: false))}.join()}}
The context is a set: + is union with right-hand override, and a child inherits everything except the Job, which is replaced by a fresh child Job.
Arena IDEimport kotlinx.coroutines.*suspend fun blockingLookup(id: Int): String {Thread.sleep(10) // a genuinely blocking callreturn "row-" + id}fun main() = runBlocking {// Offload the blocking call, get the value back, carry on sequentially.val row = withContext(Dispatchers.IO) { blockingLookup(1) }println("withContext returned " + row)// launch does NOT return the value — it returns a Job, and the code after// it runs immediately.var captured: String? = nullval job = launch(Dispatchers.IO) { captured = blockingLookup(2) }println("right after launch, captured = " + captured)job.join()println("after join, captured = " + captured)}
withContext is a temporary override that returns a value; launch forks a child that returns a Job. Confusing the two is how sequential code becomes fire-and-forget.
Arena IDEimport kotlinx.coroutines.*import java.util.concurrent.atomic.AtomicInteger@OptIn(ExperimentalCoroutinesApi::class)fun main() = runBlocking {suspend fun measurePeak(dispatcher: CoroutineDispatcher): Int {val inFlight = AtomicInteger(0)val peak = AtomicInteger(0)coroutineScope {repeat(24) {launch(dispatcher) {val now = inFlight.incrementAndGet()peak.updateAndGet { if (now > it) now else it }Thread.sleep(20) // hold the thread, so the cap showsinFlight.decrementAndGet()}}}return peak.get()}val capped = Dispatchers.IO.limitedParallelism(4)println("limitedParallelism(4) peak = " + measurePeak(capped))println("(a hard ceiling, so one subsystem cannot starve the rest of IO)")}
limitedParallelism carves a capped view of a dispatcher — one subsystem gets a slice of IO instead of all of it, and the cap is observable.
Arena IDEimport kotlinx.coroutines.*val requestId = ThreadLocal<String?>()fun main() = runBlocking {requestId.set("req-42")// Plain: the value is gone the moment we resume on another thread.withContext(Dispatchers.Default) {println("without element: " + requestId.get())}// As a context element: re-installed around every resumption, on whatever// thread the coroutine lands on.withContext(Dispatchers.Default + requestId.asContextElement("req-42")) {println("with element: " + requestId.get())delay(10) // suspend, maybe move threadprintln("after suspend: " + requestId.get())}// In a Spring app this is MDCContext from kotlinx-coroutines-slf4j, which// is the same ThreadContextElement machinery applied to SLF4J's MDC.}
A ThreadLocal does not survive a suspension, because the coroutine can resume on another thread. asContextElement()/ThreadContextElement is the fix — and MDCContext is that mechanism packaged for logging.
Arena IDE🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. Inside a suspend function you set an SLF4J MDC value, then call `awaitSingle()` on a WebClient response, then log. The correlation id is missing from the second log line. Why?