Опыт написания библиотеки управления акторами на Rust

от автора

В своем последнем проекте я использовал популярную библиотеку 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *