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 теряет сообщения.

Sources

Related topics