Kotlin CoroutinesSeniorSystem design
Что такое модель акторов (actor model) и как её реализовать с помощью coroutines?
Модель акторов — паттерн конкурентности, где актор обрабатывает сообщения последовательно через собственный mailbox; в Kotlin реализуется через Channel или устаревший actor{} builder, а в современном коде — через sealed class + Channel в ViewModel.
Модель акторов (Actor Model)
Модель акторов — концепция конкурентного программирования, в которой каждый актор:
- имеет приватное состояние, недоступное извне
- общается с другими только через передачу сообщений
- обрабатывает сообщения строго последовательно (нет race conditions)
- может создавать новых акторов и отправлять им сообщения
Устаревший actor{} builder
В Kotlin Coroutines существует actor() builder, но он помечен @ObsoleteCoroutinesApi:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
sealed class CounterMsg
object IncCounter : CounterMsg()
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg()
@OptIn(ObsoleteCoroutinesApi::class)
fun CoroutineScope.counterActor() = actor<CounterMsg> {
var counter = 0
for (msg in channel) {
when (msg) {
is IncCounter -> counter++
is GetCounter -> msg.response.complete(counter)
}
}
}
fun main() = runBlocking {
val counter = counterActor()
repeat(1000) {
counter.send(IncCounter)
}
val result = CompletableDeferred<Int>()
counter.send(GetCounter(result))
println("Counter: ${result.await()}") // 1000
counter.close()
}
Современная реализация через Channel
Рекомендованная альтернатива — явный Channel с coroutine-обработчиком:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
sealed interface Action {
data class Deposit(val amount: Int) : Action
data class Withdraw(val amount: Int) : Action
data class GetBalance(val deferred: CompletableDeferred<Int>) : Action
}
class AccountActor(scope: CoroutineScope) {
private val mailbox = Channel<Action>(Channel.UNLIMITED)
private var balance = 0
init {
scope.launch {
for (action in mailbox) {
when (action) {
is Action.Deposit -> balance += action.amount
is Action.Withdraw -> {
if (balance >= action.amount) balance -= action.amount
}
is Action.GetBalance -> action.deferred.complete(balance)
}
}
}
}
suspend fun deposit(amount: Int) = mailbox.send(Action.Deposit(amount))
suspend fun withdraw(amount: Int) = mailbox.send(Action.Withdraw(amount))
suspend fun getBalance(): Int {
val d = CompletableDeferred<Int>()
mailbox.send(Action.GetBalance(d))
return d.await()
}
fun close() = mailbox.close()
}
fun main() = runBlocking {
val account = AccountActor(this)
repeat(100) { launch { account.deposit(10) } }
println("Balance: ${account.getBalance()}") // 1000
account.close()
}
Применение в Android ViewModel
class CartViewModel : ViewModel() {
private val _state = MutableStateFlow(CartState())
val state: StateFlow<CartState> = _state.asStateFlow()
// Mailbox актора
private val actions = Channel<CartAction>(Channel.UNLIMITED)
init {
viewModelScope.launch {
for (action in actions) {
_state.update { reducer(it, action) }
}
}
}
fun dispatch(action: CartAction) {
actions.trySend(action) // не suspend — безопасно из UI
}
private fun reducer(state: CartState, action: CartAction): CartState =
when (action) {
is CartAction.AddItem -> state.copy(items = state.items + action.item)
is CartAction.RemoveItem -> state.copy(items = state.items - action.item)
}
}
Подводные камни
- actor{} builder помечен @ObsoleteCoroutinesApi — может быть удалён в будущих версиях.
- Неограниченный (UNLIMITED) канал может привести к OutOfMemoryError при медленном потребителе.
- Отправка в закрытый канал бросает ClosedSendChannelException — нужно обрабатывать после cancel.
- CompletableDeferred для запросов-ответов не имеет таймаута — используйте withTimeout вокруг await().
- Актор не даёт backpressure по умолчанию — медленный актор накапливает сообщения без ограничений.
- Ошибка внутри обработчика сообщений завершает весь цикл for — нужно обернуть в try/catch.
- Разделение нескольких Channel между coroutines без явной документации превращает код в запутанную сеть зависимостей.
Common mistakes
- Объяснять «actor model на coroutines» только как синтаксис и не описывать поведение runtime/compiler.
- Игнорировать важный риск: Actor может стать бутылочным горлышком, а неверная стратегия закрытия mailbox теряет сообщения.
- Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.
What the interviewer is testing
- Формулирует суть темы «actor model на coroutines» своими словами и связывает ее с кодом.
- Называет механизм: На kotlinx.coroutines это обычно Channel плюс loop receive, где все mutation происходят внутри владельца состояния.
- Видит production-последствие: Actor может стать бутылочным горлышком, а неверная стратегия закрытия mailbox теряет сообщения.