Event-driven архитектура на Rust: реальные примеры и практика

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Когда события управляют кодом: Event-driven на Rust

Представьте, что ваш код — это дирижёр оркестра. В классическом синхронном программировании дирижёр сам решает, когда какая нота должна звучать. В event-driven архитектуре дирижёр просто реагирует на то, что музыканты уже начали играть. Звучит парадоксально? Только на первый взгляд.

Event-driven (событийно-ориентированная) архитектура — это подход, при котором поток выполнения программы определяется событиями: клик мыши, получение HTTP-запроса, завершение ввода-вывода, таймер. Rust с его системой владения и нулевой стоимостью абстракций — идеальная почва для такой архитектуры. Особенно если вы пришли из Python, где event loop (например, в asyncio) работает, но часто упирается в GIL и накладные расходы на динамическую типизацию.

В этой статье мы не просто поговорим о теории. Мы напишем реальный код, разберём event-driven архитектуру на Rust с использованием популярных крейтов и покажем, как это может ускорить ваши приложения в разы.

Почему Rust, а не Python для событий?

Python — прекрасный язык для прототипирования. Но когда вам нужна обработка событий в реальном времени с тысячами одновременных подключений, Python начинает тормозить. Rust же даёт:

  • Нулевую стоимость абстракций — async/await не добавляет оверхэда, если вы не используете его возможности
  • Отсутствие сборщика мусора — нет внезапных пауз при обработке событий
  • Систему типов, которая ловит ошибки на этапе компиляции
  • Настоящий параллелизм — не кооперативная многозадачность, а реальные потоки без GIL

Если вы пишете event-driven приложения на Rust, вы получаете контроль над каждым байтом памяти и каждым тактом процессора. И это не маркетинг — это факт.

Основы: Event Loop и обработка событий

В основе любой событийно-ориентированной системы лежит event loop (цикл событий). Это бесконечный цикл, который:

  1. Ждёт появления нового события
  2. Извлекает его из очереди
  3. Передаёт обработчику
  4. Повторяет

В Rust стандартный event loop предоставляется крейтом tokio. Давайте напишем простейший пример:

use tokio::sync::mpsc;
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    // Создаём канал для передачи событий
    let (tx, mut rx) = mpsc::channel::<String>(32);

    // Продюсер: генерирует события
    tokio::spawn(async move {
        for i in 0..5 {
            let event = format!("Событие №{}", i);
            tx.send(event).await.unwrap();
            sleep(Duration::from_millis(500)).await;
        }
    });

    // Консьюмер: обрабатывает события
    while let Some(event) = rx.recv().await {
        println!("Обработано: {}", event);
        // Здесь может быть ваша бизнес-логика
    }
}

Этот код демонстрирует базовый паттерн event-driven programming в Rust: один компонент генерирует события, другой — их обрабатывает. Канал mpsc (multi-producer, single-consumer) обеспечивает безопасную передачу данных между задачами.

Продвинутый пример: система обработки заказов

Давайте усложним задачу. Представьте, что мы пишем бэкенд для интернет-магазина. Когда приходит новый заказ, нужно:

  • Проверить наличие товара
  • Зарезервировать его
  • Отправить уведомление
  • Обновить статистику

Классический подход — всё это делать синхронно. Event-driven подход — разбить на события:

use tokio::sync::broadcast;
use tokio::time::sleep;
use std::time::Duration;

#[derive(Debug, Clone)]
enum OrderEvent {
    NewOrder { id: u32, items: Vec<String> },
    PaymentReceived { order_id: u32 },
    ShipmentStarted { order_id: u32 },
    OrderCompleted { order_id: u32 },
}

#[tokio::main]
async fn main() {
    let (tx, _) = broadcast::channel::<OrderEvent>(16);

    // Подписчик 1: проверка наличия
    let mut rx1 = tx.subscribe();
    tokio::spawn(async move {
        while let Ok(event) = rx1.recv().await {
            match event {
                OrderEvent::NewOrder { id, items } => {
                    println!("[Склад] Проверка заказа #{}: {:?}", id, items);
                    sleep(Duration::from_millis(100)).await;
                    println!("[Склад] Товары в наличии, резервируем");
                }
                _ => {}
            }
        }
    });

    // Подписчик 2: логирование и уведомления
    let mut rx2 = tx.subscribe();
    tokio::spawn(async move {
        while let Ok(event) = rx2.recv().await {
            println!("[Логгер] Событие: {:?}", event);
        }
    });

    // Публикуем события
    println!("[Магазин] Новый заказ!");
    tx.send(OrderEvent::NewOrder {
        id: 1,
        items: vec!["Книга по Rust".to_string(), "Ноутбук".to_string()],
    }).unwrap();

    sleep(Duration::from_millis(200)).await;

    tx.send(OrderEvent::PaymentReceived { order_id: 1 }).unwrap();
    sleep(Duration::from_millis(200)).await;

    tx.send(OrderEvent::ShipmentStarted { order_id: 1 }).unwrap();
    sleep(Duration::from_millis(200)).await;

    tx.send(OrderEvent::OrderCompleted { order_id: 1 }).unwrap();

    sleep(Duration::from_secs(1)).await;
}

Обратите внимание: мы использовали broadcast, а не mpsc. Это позволяет нескольким подписчикам получать одни и те же события. Каждый обработчик работает независимо и параллельно.

Актёрская модель: когда событий много

Для сложных систем с сотнями типов событий удобно использовать actor model. В Rust для этого есть крейт actix (не путать с actix-web). Но можно построить простую актёрскую систему и на нативном tokio:

use tokio::sync::mpsc;
use std::collections::HashMap;

type ActorRef = mpsc::Sender<ActorMessage>;

#[derive(Debug)]
enum ActorMessage {
    Greet { name: String },
    Count,
    Stop,
}

struct CounterActor {
    count: u32,
    children: HashMap<String, ActorRef>,
}

impl CounterActor {
    fn new() -> (Self, mpsc::Receiver<ActorMessage>) {
        let (tx, rx) = mpsc::channel(32);
        let actor = CounterActor {
            count: 0,
            children: HashMap::new(),
        };
        (actor, rx)
    }

    async fn handle(&mut self, msg: ActorMessage) {
        match msg {
            ActorMessage::Greet { name } => {
                self.count += 1;
                println!("Привет, {}! Всего приветствий: {}", name, self.count);                

Блоги

Рекомендаций по книгам