Все знают:
ThreadLocal привязан к потоку. Корутины могут прыгать между потоками. Это конфликт.Большинство знает решение:
asContextElement().Но немногие задумываются, как именно это работает и почему злоупотребление этим механизмом может просадить пропускную способность сервиса.
Проблема: M:N Threading
В мире корутин один и тот же бизнес-процесс может начать выполняться на
Thread-1, приостановиться (IO), и продолжить на Thread-2.Если вы положили данные в
ThreadLocal (например, RequestID для логирования) и не передали их в контекст корутины - после саспенда вы эти данные потеряете. Или, что хуже, прочитаете "грязные" данные от другой корутины, которая использовала этот поток ранее.Решение: ThreadContextElement
Kotlin предлагает мост:
ThreadLocal.asContextElement(value).Выглядит просто, но под капотом происходит постоянное перекладывание данных.
val myTL = ThreadLocal<String>()
launch(Dispatchers.Default + myTL.asContextElement("foo")) {
println(myTL.get()) // "foo"
delay(10) // Suspend -> поток освобождается
println(myTL.get()) // "foo" (хотя поток может быть уже другим)
}
Что происходит в
DispatchedContinuation?Каждый раз, когда корутина возобновляется (resume) на потоке, срабатывает механизм перехватчиков.
1. Mount (updateThreadContext): Перед выполнением блока кода значение из контекста корутины принудительно устанавливается в
ThreadLocal текущего потока. Старое значение потока запоминается.2. Execution: Код корутины выполняется.
3. Unmount (restoreThreadContext): Как только корутина снова саспендится или завершается, в
ThreadLocal потока возвращается старое значение.Это гарантирует, что мы не загрязняем потоки пула.
📉 Скрытый оверхед (Performance Penalty)
Это не бесплатно. Если у вас в стеке корутины 5 элементов
ThreadContextElement (MDC, SecurityContext, Tracing, TenantId и т.д.), и ваша корутина делает 100 саспендов (например, частые мелкие IO или yield()), то 100 раз произойдет цикл:Save Old State -> Set New State -> Run -> Restore Old State.На высоконагруженных системах (High Throughput) это создает ощутимое давление, так как эти операции часто включают работу с
ThreadLocalMap, что не так быстро, как доступ к регистрам.Best Practices для Senior-разработчика
1. Не используйте ThreadLocal как шину данных. Передавайте явные параметры в функции (
context receivers в будущем помогут).2. Ограничьте Scope. Используйте
withContext(myTL.asContextElement()) только вокруг тех участков кода, где это действительно нужно (например, вызов легаси библиотеки, зависящей от ThreadLocal), а не на уровне всего GlobalScope или корневой корутины.3. MDC Integration. Для логирования лучше использовать специализированные библиотеки, которые умеют эффективно работать с контекстом, или копировать контекст только на границах системы, а не таскать его везде.
Вывод:
asContextElement - это костыль для совместимости с Thread-bound миром. В чистом Coroutine-мире данные должны течь через аргументы или CoroutineContext, но не через ThreadLocal.#kotlin #coroutines #concurrency #jvm #senior
✍️ @kotlin_lib