Почему этот репозиторий навсегда зависает на первом же вызове user()?
Этот репозиторий держит все обращения к базе на одном выделенном потоке. Первый же вызов user() не возвращается, и поток больше не освобождается — приложение зависает.
Ограничения: сохранить работу с базой на одном потоке, не заводить второй dispatcher и нигде не блокировать поток.
class UserRepository(private val db: Db) {
private val dbDispatcher = newSingleThreadContext("db")
private val cache = mutableMapOf<Long, User>()
suspend fun user(id: Long): User = withContext(dbDispatcher) {
cache[id] ?: load(id)
}
private fun load(id: Long): User = runBlocking {
val fresh = withContext(dbDispatcher) { db.load(id) }
cache[id] = fresh
fresh
}
}
Найдите и исправьте ошибку.
load вызывается, когда мы уже на dbDispatcher, а runBlocking блокирует этот единственный поток. Внутренний withContext(dbDispatcher) ставит работу в очередь тому же запаркованному потоку — двигаться некому, это deadlock. Сделайте load функцией suspend, зовите db.load(id) напрямую и уберите runBlocking.
- ✗Звать
suspend-функцию черезrunBlockingиз обычного кода, который и так уже исполняется внутри корутины - ✗Думать, что
runBlockingприостанавливает вызывающего, а не блокирует его поток - ✗Считать, что
withContextна тот же dispatcher, где вы уже находитесь, всегда бесплатен
- →Почему та же вложенность даёт deadlock на
Dispatchers.Main, но часто выживает наDispatchers.IO? - →Как сделать этот deadlock детерминированным в модульном тесте?
Баг
user() уже исполняется на dbDispatcher — единственном потоке. Оттуда вызывается обычная функция load, которая через runBlocking блокирует этот поток и ждёт своё тело. А тело делает withContext(dbDispatcher) — ставит задачу в очередь тому же самому потоку, который сейчас запаркован внутри runBlocking. Разобрать очередь некому: deadlock.
suspend fun user(id: Long): User = withContext(dbDispatcher) { // поток "db" занят
cache[id] ?: load(id)
}
private fun load(id: Long): User = runBlocking { // ❌ блокирует поток "db"
val fresh = withContext(dbDispatcher) { db.load(id) } // ❌ ждёт поток "db"
cache[id] = fresh
fresh
}
Исправление
Убрать блокирующий мост: load становится suspend-функцией и вызывается напрямую. Она и так исполняется на dbDispatcher, поэтому вложенный withContext не нужен — обращение к базе остаётся на одном потоке, но поток нигде не блокируется.
suspend fun user(id: Long): User = withContext(dbDispatcher) {
cache[id] ?: load(id)
}
private suspend fun load(id: Long): User { // ✅ suspend, без runBlocking
val fresh = db.load(id) // ✅ уже на потоке "db"
cache[id] = fresh
return fresh
}
runBlocking — мост из блокирующего мира в мир корутин: ему место в main и в тестах, а не внутри корутины. На однопоточном dispatcher такой мост гарантированно упирается в самого себя.