Dispatchers.Unconfined является одним из встроенных CoroutineDispatchers, входящих в kotlinx.coroutines. Он отличается от других встроенных диспетчеров тем, что не опирается на какой-то конкретный пул потоков. Вместо этого Dispatchers.Unconfined спроектирован так, чтобы никогда не менять поток при входе в его контекст (это называется “диспетчеризацией”). В этом легко убедиться, если посмотреть на его упрощённую реализацию ниже:
object Unconfined : CoroutineDispatcher() {
override fun isDispatchNeeded(context: CoroutineContext) = false
override fun dispatch(context: CoroutineContext, block: Runnable) {
throw UnsupportedOperationException()
}
}
Это поведение отличается от того, что реализовано в Dispatchers.Main или Dispatchers.Default, которые меняют потоки, если не находятся в одном из предпочтительных. В результате код, выполняющийся в Dispatchers.Unconfined всегда будет выполняться синхронно при входе в его контекст.
На практике это означает, что любой код внутри Dispatchers.Unconfined не имеет никаких гарантий относительно того, в каком потоке он будет выполняться. Это может приводить к неуловимым багам, поскольку диспетчеризация происходит как при входе в новый контекст, так и возвращении из него. Рассмотрим пример, в котором мы будем читать некоторый текст на IO диспетчере и затем обновлять результат в главном потоке:
// Представьте, что эти диспетчеры встроены
val ioDispatcher = Dispatchers.IO
val mainDispatcher = Dispatchers.Main
withContext(ioDispatcher) {
val firstText = readFile(1)
val secondText = readFile(2)
withContext(mainDispatcher) {
textView.text = firstText
delay(1.seconds)
textView.text = secondText
}
}
Если мы тестируем эту функцию, например в скриншот-тесте, и знаем, что наш тест запускается на главном потоке, то мы можем захотеть полностью избежать диспетчеризации, чтобы наш тест выполнялся синхронно на вызывающем диспетчере. Для этого заменим Dispatchers.IO и Dispatchers.Main на Dispatchers.Unconfined.
val ioDispatcher = Dispatchers.Unconfined
val mainDispatcher = Dispatchers.Unconfined
withContext(ioDispatcher) {
val firstText = readFile(1)
val secondText = readFile(2)
withContext(mainDispatcher) {
textView.text = firstText
delay(1.seconds)
textView.text = secondText // В этой строке произойдёт креш!
}
}
Однако это изменение приведёт к сбою, поскольку метод delay() внутри меняет контекст на Dispatchers.Default, а поскольку мы используем Dispatchers.Unconfined, то мы не вернём результат обратно в главный поток. Когда мы попытаемся обновить текст у textView, возникнет исключение CalledFromWrongThreadException.
Этот пример хорошо показывает, как Dispatchers.Unconfined нарушает одно из лучших свойств корутин: работу с потоками внутри библиотеки. Когда мы используем Dispatchers.Main или Dispatchers.Default, нам не нужно беспокоиться о диспетчеризации в нужный поток после вызова очередной suspend функции – это уже реализовано за нас.
Есть лучший способ
Обычно мы используем withContext() для изменения CoroutineDispatcher, но на самом деле метод принимает в качестве параметра CoroutineContext. CoroutineContext можно считать эквивалентом Map<CoroutineContext.Key, CoroutineContext.Element>. Когда мы вызываем withContext(Dispatchers.Unconfined), мы переписываем ключ CoroutineDispatcher текущего контекста на Dispatchers.Unconfined.
Вместо этого мы должны использовать EmptyCoroutineContext, поскольку он не обновляет CoroutineDispatcher текущего контекста. Таким образом, при вызове withContext(EmptyCoroutineContext) диспетчеризация не происходит, потому что контекст корутины не меняется. Но при этом мы всё равно вернёмся назад в нужный поток, если другой метод, например delay(), изменит контекст. Давайте рассмотрим приведённый выше пример, используя EmptyCoroutineContext вместо Dispatchers.Unconfined:
val ioDispatcher = EmptyCoroutineContext
val mainDispatcher = EmptyCoroutineContext
withContext(ioDispatcher) {
val firstText = readFile(1)
val secondText = readFile(2)
withContext(mainDispatcher) {
textView.text = firstText
delay(1.seconds)
textView.text = secondText // Does not crash.
}
}
Использование EmptyCoroutineContext позволяет нам продолжить синхронное выполнение на главном потоке и избежать сбоев, так как мы корректно меняем поток обратно на главный после вызова delay().
На самом деле, существует очень мало случаев, когда нам вообще нужно ссылаться на класс CoroutineDispatcher. CoroutineScope(), withContext() и CoroutineContext.plus принимают в качестве параметра CoroutineContext. CoroutineContext более гибкий, так как в него можно добавить другие элементы, например CoroutineName для отладки. Я бы рекомендовал заменить все ваши ссылки с CoroutineDispatcher на CoroutineContext – особенно если вы поддерживаете публичный API. К примеру, Coil обновил свой публичный API, чтоб принимать CoroutineContext вместо CoroutineDispatcher в версии 3.0.