When Events Control the Code: Event-driven in Rust
\n\nImagine that your code is an orchestra conductor. In classical synchronous programming, the conductor decides when each note should play. In event-driven architecture, the conductor simply reacts to the fact that the musicians have already started playing. Sounds paradoxical? Only at first glance.
\n\nEvent-driven architecture is an approach where the program's execution flow is determined by events: a mouse click, receiving an HTTP request, completing I/O, a timer. Rust, with its ownership system and zero-cost abstractions, is an ideal foundation for such architecture. Especially if you come from Python, where the event loop (e.g., in asyncio) works but often runs into the GIL and the overhead of dynamic typing.
\n\nIn this article, we won't just talk about theory. We will write real code, break down event-driven architecture in Rust using popular crates, and show how it can speed up your applications significantly.
\n\nWhy Rust, Not Python, for Events?
\n\nPython is an excellent language for prototyping. But when you need real-time event processing with thousands of concurrent connections, Python starts to slow down. Rust, on the other hand, provides:
\n\n- \n
- Zero-cost abstractions — async/await adds no overhead if you don't use its features \n
- No garbage collector — no sudden pauses during event processing \n
- A type system that catches errors at compile time \n
- True parallelism — not cooperative multitasking, but real threads without a GIL \n
If you write event-driven applications in Rust, you get control over every byte of memory and every CPU cycle. And this isn't marketing — it's a fact.
\n\nBasics: Event Loop and Event Processing
\n\nAt the heart of any event-driven system lies the event loop. This is an infinite loop that:
\n\n- \n
- Waits for a new event to appear \n
- Retrieves it from the queue \n
- Passes it to the handler \n
- Repeats \n
In Rust, the standard event loop is provided by the tokio crate. Let's write a simple example:
use tokio::sync::mpsc;\nuse tokio::time::{sleep, Duration};\n\n#[tokio::main]\nasync fn main() {\n // Create a channel for passing events\n let (tx, mut rx) = mpsc::channel::<String>(32);\n\n // Producer: generates events\n tokio::spawn(async move {\n for i in 0..5 {\n let event = format!("Event №{}", i);\n tx.send(event).await.unwrap();\n sleep(Duration::from_millis(500)).await;\n }\n });\n\n // Consumer: processes events\n while let Some(event) = rx.recv().await {\n println!("Processed: {}", event);\n // Your business logic can go here\n }\n}\n\nThis code demonstrates the basic event-driven programming pattern in Rust: one component generates events, another processes them. The mpsc (multi-producer, single-consumer) channel ensures safe data transfer between tasks.
Advanced Example: Order Processing System
\n\nLet's make the task more complex. Imagine we are writing a backend for an online store. When a new order arrives, we need to:
\n\n- \n
- Check product availability \n
- Reserve it \n
- Send a notification \n
- Update statistics \n
The classic approach is to do all this synchronously. The event-driven approach is to break it down into events:
\n\nuse tokio::sync::broadcast;\nuse tokio::time::sleep;\nuse std::time::Duration;\n\n#[derive(Debug, Clone)]\nenum OrderEvent {\n NewOrder { id: u32, items: Vec<String>