В своем последнем проекте я использовал популярную библиотеку Actix для реализации модели акторов. Однако в процессе разработки я столкнулся с ограничениями.

Обработчики сообщений в Actix реализуются через трейт Handler, который требует синхронных методов, а не асинхронных функций. Для использования асинхронных вещей это крайне не удобно.
В Actix все акторы по умолчанию размещаются в одном потоке. Чтобы запустить акторы в отдельных потоках, можно создать отдельный Arbiter для каждого актора, это даст изолированный поток. А для моих потребностей нужно, чтобы каждый актор работал в своем потоке и чтобы они не были изолированы из-за этого для общения.
В итоге, я решил переписать свой код с использованием библиотеки Tokio Mpsc. Чтобы не дублировать общую логику взаимодействия акторов в разных частях проекта, я вынес ее в отдельную библиотеку. В отличие от Actix, я реализовал ее не как Фреймворк, а как чистую библиотеку общего назначения.
Всем известны различия между фреймворком и библиотекой в программировании:
Фреймворк
-
Предоставляет скелет приложения и определяет поток управления. Разработчик должен вписать свой код в предопределенные места.
-
Имеет инверсию управления — фреймворк вызывает код разработчика.
-
Обычно сложнее в освоении, чем библиотеки.
Плюсы:
-
Ускоряет разработку за счет готовой архитектуры и инструментов.
-
Поощряет следование лучшим практикам и паттернам проектирования.
-
Облегчает тестирование и поддержку кода.
Минусы:
-
Высокая избыточность для простых проектов.
-
Сложность в освоении из-за большого количества функций.
-
Привязка к конкретному фреймворку.
Библиотека
-
Предоставляет набор готовых классов и функций для решения частных задач.
-
Разработчик вызывает код библиотеки. Инверсия управления отсутствует.
-
Проще в освоении и применении, чем фреймворки.
Плюсы:
-
Легкая интеграция в существующие проекты.
-
Возможность выбрать только нужный набор функций.
-
Низкая привязка к конкретной библиотеке.
Минусы:
-
Требуется самостоятельно писать основной код приложения.
-
Отсутствие готовых шаблонов и архитектуры.
-
Трудности с поддержкой кода из-за отсутствия общих паттернов.
В целом, я устал от фреймворков и решил писать библиотеку. Надоело упираться в концептуальные ограничения от них.
Асинхронная коммуникация через функции send и ask
Одной из ключевых особенностей моей библиотеки является поддержка асинхронной коммуникации между акторами. Коммуникация реализована через функции send и ask. Функция send позволяет отправить сообщение актору «послал и забыл» стилем, в то время как функция ask возвращает Future с ожиданием результата обработки сообщения.
Взглянем на пример кода, который демонстрирует это:
#[derive(Debug)] pub struct UserActor; #[derive(Debug)] pub enum UserMessage { CreateAccount { account_id: u32, } , GetBalance { account_id: u32, }, MoveMoney { from_account_id: u32, to_account_id: u32, amount: u32 }, } #[derive(Debug)] pub enum UserResponse { Balance { amount: u32, }, AccountCreated { account_id: u32, }, Ok, } #[derive(Debug,Clone)] pub struct UserState { pub name: String, } #[derive(Error, Debug)] pub enum UserError { #[error("unknown error")] Unknown, #[error("std::io::Error")] StdErr(#[from] std::io::Error), } #[async_trait] impl Handler<UserActor, UserMessage, UserState, UserResponse, UserError> for UserActor { async fn receive(&self, ctx: Context<UserActor, UserMessage, UserState, UserResponse, UserError>) -> Result<UserResponse, UserError> { match ctx.mgs { UserMessage::GetBalance { .. } => { Ok(UserResponse::Balance { amount: 100 }) } _ => {Ok(UserResponse::Ok)} } } } #[tokio::main] async fn main() -> Result<(), EchoError> { let mut user:Arc<ActorRef<UserActor, UserMessage, UserState, UserResponse, UserError>> = ActorRef::new("user".to_string(), UserActor {}, UserState {name: "".to_string()}, 10000).await; user.send(UserMessage::CreateAccount{ account_id: 0 }).await?; let result: UserResponse = user.ask(UserMessage::CreateAccount{ account_id: 0 }).await?; }
Управление состоянием актора: блокировка и изменение
Традиционно, доступ к состоянию актора ограничен и осуществляется только внутри актора. Однако, в моей библиотеке я предоставил возможность получить блокировку на состояние актора и даже внести изменения в него. Это оказывается очень полезным в ситуациях, когда необходимо мгновенно получить данные из актора, даже если его входящая очередь сообщений переполнена.
Пример кода, демонстрирующего эту функциональность:
let actor_state = user.state().await?; let state_lock = actor_state.lock().await; let name_from_state = state_lock.name.clone();
В заключение можно сказать, что библиотека управления акторами на Rust, которую я написал, обладает следующими преимуществами:
-
Асинхронная коммуникация между акторами.
-
Возможность блокировки и изменения состояния актора.
-
Простота в использовании.
Код библиотеки доступен по ссылке.
ссылка на оригинал статьи https://habr.com/ru/articles/755704/
Добавить комментарий