---
title: Диспетчеры и контекст
description: CoroutineContext как набор элементов, выбор между Default, IO и Main, limitedParallelism и наследование контекста
tags:
- kotlin
- coroutines
---
`CoroutineContext` - неизменяемый набор элементов, описывающий, как и где выполняется корутина. Диспетчер - один из этих элементов, отвечающий за поток.
```kotlin
import kotlinx.coroutines.*
import kotlin.coroutines.ContinuationInterceptor
fun main() = runBlocking {
println(coroutineContext[ContinuationInterceptor] != null) // true
println(coroutineContext[Job] != null) // true
println(coroutineContext[CoroutineName]) // null — имени не задали
withContext(CoroutineName("загрузка")) {
println(coroutineContext[CoroutineName]?.name) // загрузка
}
}
```
Элементы складываются оператором `+`, а извлекаются по ключу как из `Map`. Ключ диспетчера называется `ContinuationInterceptor` - это более общее понятие «перехватчик продолжений», частным случаем которого является диспетчер. Ключ `CoroutineDispatcher` тоже существует, но помечен `@ExperimentalStdlibApi`.
## Из чего состоит контекст
| Элемент | Зачем |
|---|---|
| `Job` | жизненный цикл, иерархия, отмена |
| `CoroutineDispatcher` | на каком потоке выполнять |
| `CoroutineName` | имя для отладки и логов |
| `CoroutineExceptionHandler` | что делать с необработанной ошибкой |
```kotlin
import kotlinx.coroutines.*
import kotlin.coroutines.ContinuationInterceptor
fun main() = runBlocking {
val context = Dispatchers.IO + CoroutineName("задача") + SupervisorJob()
withContext(context) {
println(coroutineContext[CoroutineName]?.name) // задача
println(coroutineContext[ContinuationInterceptor] == Dispatchers.IO) // true
}
}
```
Сложение не коммутативно по смыслу: при конфликте элементов одного типа побеждает **правый**.
```kotlin
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
fun main() {
val base: CoroutineContext = Dispatchers.IO + CoroutineName("первое")
val combined = base + CoroutineName("второе")
println(combined[CoroutineName]?.name) // второе
}
```
Для двух диспетчеров подряд (`Dispatchers.IO + Dispatchers.Default`) компилятор выдаёт предупреждение о бессмысленности выражения - правило то же, побеждает правый, но писать так незачем.
## Стандартные диспетчеры
| Диспетчер | Потоков | Для чего |
|---|---|---|
| `Dispatchers.Default` | по числу ядер (минимум 2) | вычисления, обработка данных |
| `Dispatchers.IO` | до 64 (растёт по требованию) | блокирующий I/O: файлы, JDBC, legacy-HTTP |
| `Dispatchers.Main` | один, платформенный | UI (Android, Swing, JavaFX) |
| `Dispatchers.Unconfined` | любой, текущий | редкие случаи, не для продакшена |
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
println("runBlocking: ${Thread.currentThread().name}")
withContext(Dispatchers.Default) {
println("Default: ${Thread.currentThread().name.contains("DefaultDispatcher")}")
}
withContext(Dispatchers.IO) {
println("IO: ${Thread.currentThread().name.contains("DefaultDispatcher")}")
}
// runBlocking: main
// Default: true
// IO: true
}
```
Обратите внимание: и `Default`, и `IO` показывают `DefaultDispatcher-worker-N` - они **делят один пул потоков**. `IO` - это тот же пул с другим лимитом параллелизма, поэтому переключение между `Default` и `IO` дешёвое и не требует смены потока.
## Default против IO
Выбор определяется одним вопросом: код считает или ждёт?
```kotlin
import kotlinx.coroutines.*
// CPU-bound: больше потоков, чем ядер, только вредит
suspend fun compute(): Long = withContext(Dispatchers.Default) {
(1..1_000_000).sumOf { it.toLong() }
}
// блокирующий I/O: поток простаивает, поэтому их нужно много
suspend fun readBlocking(): String = withContext(Dispatchers.IO) {
Thread.sleep(20) // имитация JDBC или File.readText
"прочитано"
}
fun main() = runBlocking {
println(compute()) // 500000500000
println(readBlocking()) // прочитано
}
```
Логика лимитов: на `Default` потоков столько же, сколько ядер, потому что больше вычислять всё равно некому - лишние потоки только добавят переключений контекста. На `IO` потоки в основном спят в ожидании ответа, поэтому их держат до 64.
Если API уже `suspend` и не блокирует (Ktor-клиент, R2DBC, `kotlinx-io`), диспетчер менять не нужно вообще - это лишнее переключение.
## limitedParallelism
Ограничивает число одновременных корутин, не создавая новых потоков:
```kotlin
import kotlinx.coroutines.*
import java.util.concurrent.atomic.AtomicInteger
fun main() = runBlocking {
val limited = Dispatchers.IO.limitedParallelism(2)
val running = AtomicInteger(0)
val maxRunning = AtomicInteger(0)
withContext(limited) {
repeat(10) {
launch {
val now = running.incrementAndGet()
maxRunning.updateAndGet { m -> maxOf(m, now) }
Thread.sleep(20) // занимает поток
running.decrementAndGet()
}
}
}
println("одновременно выполнялось не больше 2: ${maxRunning.get() <= 2}")
// одновременно выполнялось не больше 2: true
}
```
Это замена старому `newFixedThreadPoolContext`: вместо отдельного пула используется общий, а ограничивается только параллелизм. Типичное применение - не перегрузить базу данных или внешний API.
Важная тонкость: лимит считает корутины, **занимающие поток**, а не существующие. Приостановленная корутина слот освобождает:
```kotlin
import kotlinx.coroutines.*
import java.util.concurrent.atomic.AtomicInteger
fun main() = runBlocking {
val limited = Dispatchers.IO.limitedParallelism(2)
val inFlight = AtomicInteger(0)
val maxInFlight = AtomicInteger(0)
withContext(limited) {
repeat(10) {
launch {
val now = inFlight.incrementAndGet()
maxInFlight.updateAndGet { m -> maxOf(m, now) }
delay(20) // слот освобождён
inFlight.decrementAndGet()
}
}
}
println("одновременно в полёте больше 2: ${maxInFlight.get() > 2}")
// одновременно в полёте больше 2: true
}
```
Поэтому `limitedParallelism` - не семафор на число запросов. Если нужно ограничить именно количество одновременных обращений к внешнему API (включая время ожидания ответа), нужен `Semaphore` из `kotlinx.coroutines.sync`. Подробнее: [Разделяемое состояние](Разделяемое%20состояние.md)
## Dispatchers.Main
```kotlin
// Android или Swing — требует соответствующей зависимости
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0")
//
// viewModelScope.launch { // уже на Main
// val data = withContext(Dispatchers.IO) { repository.load() }
// textView.text = data // снова на Main
// }
```
Главный шаблон UI-программирования: корутина стартует на `Main`, тяжёлую работу уводит в `withContext`, результат обрабатывает снова на `Main` - без коллбэков и `runOnUiThread`. Без платформенной зависимости `Dispatchers.Main` бросит `IllegalStateException` при первом обращении.
## Unconfined
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking {
withContext(Dispatchers.Unconfined) {
println("до delay: ${Thread.currentThread().name}")
delay(10)
println("после delay на потоке таймера: ${Thread.currentThread().name.contains("DefaultExecutor")}")
}
// до delay: main
// после delay на потоке таймера: true
}
```
`Unconfined` не привязывает корутину к потоку: до первой приостановки она идёт в текущем, а после возобновляется в том потоке, который её разбудил. Это ломает любые предположения о потоках, поэтому в продакшене не используется - только в тестах и специальных случаях.
## Наследование контекста
Дочерняя корутина наследует контекст родителя, заменяя только `Job`:
```kotlin
import kotlinx.coroutines.*
import kotlin.coroutines.ContinuationInterceptor
fun main() = runBlocking {
withContext(Dispatchers.IO + CoroutineName("родитель")) {
launch {
println("имя: ${coroutineContext[CoroutineName]?.name}") // имя: родитель
val inherited = coroutineContext[ContinuationInterceptor] == Dispatchers.IO
println("диспетчер унаследован: $inherited") // диспетчер унаследован: true
}
launch(CoroutineName("ребёнок")) {
println("переопределено: ${coroutineContext[CoroutineName]?.name}") // переопределено: ребёнок
}
}
}
```
`Job` ребёнка всегда новый и становится дочерним по отношению к `Job` родителя - именно так строится дерево. Подробнее: [Структурированная конкурентность](Структурированная%20конкурентность.md)
```mermaid
graph TD
A["Контекст родителя:
Job#1 + Dispatchers.IO + Name"] --> B["launch { }"]
B --> C["Контекст ребёнка:
Job#2 (parent = Job#1)
+ Dispatchers.IO + Name"]
A --> D["launch(Dispatchers.Default) { }"]
D --> E["Контекст ребёнка:
Job#3 (parent = Job#1)
+ Dispatchers.Default + Name"]
```
## Свои элементы контекста
Элемент контекста - способ передать данные через всё дерево корутин:
```kotlin
import kotlinx.coroutines.*
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.coroutineContext
class RequestId(val value: String) : AbstractCoroutineContextElement(RequestId) {
companion object Key : CoroutineContext.Key
}
suspend fun logWithId(message: String) {
val id = coroutineContext[RequestId]?.value ?: "нет id"
println("[$id] $message")
}
fun main() = runBlocking {
withContext(RequestId("req-42")) {
logWithId("начало обработки")
launch { logWithId("из дочерней корутины") }
}
logWithId("вне контекста")
// [req-42] начало обработки
// [req-42] из дочерней корутины
// [нет id] вне контекста
}
```
Это корутинный аналог `ThreadLocal`, но работающий правильно при переключении потоков. Применяется для трассировки запросов, идентификаторов пользователя, локали.
## ThreadLocal и корутины
```kotlin
import kotlinx.coroutines.*
val threadLocal = ThreadLocal()
fun main() = runBlocking {
threadLocal.set("главный")
println("до: ${threadLocal.get()}")
withContext(Dispatchers.Default) {
println("на другом потоке: ${threadLocal.get()}") // null — другой поток
}
withContext(Dispatchers.Default + threadLocal.asContextElement("перенесённый")) {
println("через asContextElement: ${threadLocal.get()}") // перенесённый
}
// до: главный
// на другом потоке: null
// через asContextElement: перенесённый
}
```
`ThreadLocal` привязан к потоку, а корутина потоки меняет - поэтому напрямую он не работает. `asContextElement()` переносит значение при каждом переключении. Это важно для MDC в логировании и Spring Security.
## Проверка потока в отладке
```kotlin
import kotlinx.coroutines.*
fun main() = runBlocking(CoroutineName("корень")) {
println(coroutineContext[CoroutineName]?.name) // корень
launch(CoroutineName("дочерняя")) {
println(coroutineContext[CoroutineName]?.name) // дочерняя
}.join()
}
```
Запуск с `-Dkotlinx.coroutines.debug` добавляет к имени потока идентификатор корутины (`main @корень#1`), что сильно упрощает чтение логов.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `ExecutorService` передаётся аргументом | диспетчер наследуется из контекста |
> | `Executors.newFixedThreadPool(n)` | `Dispatchers.Default` / `limitedParallelism(n)` |
> | `Executors.newCachedThreadPool()` | `Dispatchers.IO` |
> | `SwingUtilities.invokeLater`, `runOnUiThread` | `Dispatchers.Main` |
> | `ThreadLocal` | элемент контекста |
> | `MDC` в логировании | `MDCContext` или свой элемент |
> | пул = потоки | диспетчер = политика, потоки общие |
>
> **Пул потоков против диспетчера.** В Java каждый `ExecutorService` владеет своими потоками: четыре пула по 10 потоков - это 40 реальных потоков, даже если 35 из них простаивают. `Dispatchers.Default` и `Dispatchers.IO` делят один пул, а `limitedParallelism` ограничивает только число одновременных задач. Поэтому в Kotlin «ограничить параллелизм до 2 для работы с БД» не стоит двух простаивающих потоков.
>
> **Контекст наследуется, пул - нет.** В Java пул нужно передавать в каждый асинхронный вызов, и забытый аргумент молча уводит задачу в `ForkJoinPool.commonPool`:
> ```java
> CompletableFuture.supplyAsync(this::load); // commonPool!
> CompletableFuture.supplyAsync(this::load, myExecutor); // правильно
> ```
> Эти две строки отличаются одним аргументом и дают совершенно разное поведение под нагрузкой. В Kotlin дочерняя корутина получает диспетчер родителя автоматически, а переопределение - явное и видимое.
>
> **`ThreadLocal` в асинхронном коде в Java не работает так же** - и там это обходят сложнее. При передаче задачи в пул `ThreadLocal` теряется; Spring решает это через `TaskDecorator` и копирование `SecurityContext`/MDC в каждом месте. В Kotlin элемент контекста путешествует вместе с корутиной по всему дереву, а `asContextElement()` переносит и настоящий `ThreadLocal`, когда он нужен библиотеке.
>
> **Переключение потока явное и дешёвое.** В Java чтобы вернуться в UI-поток, нужен `invokeLater` или `Handler.post` с коллбэком, который разрывает линейность кода. `withContext(Dispatchers.Main)` - это обычный блок, после которого выполнение продолжается на нужном потоке:
> ```kotlin
> import kotlinx.coroutines.*
>
> suspend fun loadAndRender(): String {
> val data = withContext(Dispatchers.IO) { Thread.sleep(10); "данные" }
> return "отрисовано: $data" // снова на исходном диспетчере
> }
>
> fun main() = runBlocking { println(loadAndRender()) } // отрисовано: данные
> ```
>
> Сравнить: [Взаимодействие между потоками в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
> [!warning] Подводные камни
> **Блокирующий вызов на `Dispatchers.Default` исчерпывает пул:**
> ```kotlin
> import kotlinx.coroutines.*
> import kotlin.system.measureTimeMillis
>
> fun main() = runBlocking {
> val t = measureTimeMillis {
> withContext(Dispatchers.Default.limitedParallelism(2)) {
> List(6) { launch { Thread.sleep(50) } }.forEach { it.join() }
> }
> }
> println("сериализовалось вместо параллели: ${t >= 150}") // сериализовалось вместо параллели: true
> }
> ```
> Шесть задач по 50 мс на двух потоках заняли 150 мс вместо 50. На реальном `Default` (число ядер) блокирующий код точно так же съедает все потоки, и вся программа встаёт - включая несвязанные корутины.
>
> **`Dispatchers.IO` не делает блокирующий код неблокирующим.** Он лишь даёт больше потоков. 64 одновременных JDBC-запроса всё равно упрутся в размер пула соединений, а 1000 корутин на `IO` встанут в очередь.
>
> **`Dispatchers.Main` падает без платформенной зависимости:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() {
> val error = runCatching { runBlocking { withContext(Dispatchers.Main) { } } }
> .exceptionOrNull()
> println(error != null) // true
> }
> ```
> Сообщение - «Module with the Main dispatcher is missing». Нужен `kotlinx-coroutines-android`, `-javafx` или `-swing`.
>
> **`newSingleThreadContext` создаёт настоящий поток, который нужно закрывать:**
> ```kotlin
> import kotlinx.coroutines.*
>
> fun main() = runBlocking {
> @OptIn(DelicateCoroutinesApi::class)
> val dispatcher = newSingleThreadContext("мой-поток")
> try {
> withContext(dispatcher) {
> println(Thread.currentThread().name) // мой-поток
> }
> } finally {
> dispatcher.close() // без этого поток живёт до конца процесса
> }
> }
> ```
> API помечено `@DelicateCoroutinesApi` именно из-за этой утечки. Почти всегда вместо него нужен `limitedParallelism(1)`.
>
> **`Unconfined` возобновляется в неизвестном потоке.** Код после `delay` окажется на потоке таймера, после `Channel.receive` - на потоке отправителя. Любая работа с UI или `ThreadLocal` при этом ломается непредсказуемо.
>
> **`withContext` внутри цикла - лишние переключения:**
> ```kotlin
> import kotlinx.coroutines.*
> import kotlin.system.measureTimeMillis
>
> fun main() = runBlocking {
> val inside = measureTimeMillis {
> repeat(10_000) { withContext(Dispatchers.Default) { it } }
> }
> val outside = measureTimeMillis {
> withContext(Dispatchers.Default) { repeat(10_000) { } }
> }
> println("снаружи быстрее: ${outside < inside}") // снаружи быстрее: true
> }
> ```
>
> **Передача `Job` в `withContext` рвёт структурированность.** `withContext(Dispatchers.IO + Job())` создаёт корутину без родителя - отмена сверху до неё не дойдёт. Передавайте только диспетчер и имя.
>
> **Контекст неизменяем, `+` создаёт новый.** `context + CoroutineName("x")` не меняет исходный объект; забытое присваивание результата - тихая ошибка.
>
> **`ThreadLocal` без `asContextElement` теряется при любом переключении**, в том числе при возобновлении после `delay` на другом потоке. Для MDC в логах это значит, что идентификатор запроса исчезнет из части записей - используйте `MDCContext` из `kotlinx-coroutines-slf4j`.
>
> **Число потоков `IO` настраивается только через системное свойство** (`kotlinx.coroutines.io.parallelism`) и влияет на весь процесс. Для ограничения конкретного места используйте `limitedParallelism`.
>
> **Переключение диспетчера для неблокирующего API - чистая потеря.** Если библиотека уже `suspend` и не блокирует, `withContext(Dispatchers.IO)` вокруг неё добавляет переключение контекста и ничего не даёт.
Далее: [Отмена и таймауты](Отмена%20и%20таймауты.md)