Introduction to Task Queues in Rust
\n\nTask queues are one of the fundamental patterns of concurrent programming. They allow you to organize efficient work transfer between threads, simplifying application architecture. In Rust, with its strict ownership system and types, working with queues requires a special approach, but provides incredible safety and performance in return.
\n\nIn this article, we will explore how to create and use task queues in Rust from scratch. You will learn about the language's standard tools, such as mpsc (multi-producer, single-consumer) channels, and get acquainted with popular libraries for asynchronous processing. The material is intended for beginner developers already familiar with the basics of Rust syntax.
Basics: What is a Task Queue and Why Do You Need It
\n\nA task queue is a data structure that operates on the FIFO (First In, First Out) principle. In the context of multithreading, it allows some threads (producers) to add tasks, while others (consumers) retrieve and execute them.
\n\nMain advantages of using queues:
\n\n- \n
- Separation of concerns — the logic of generating tasks is separated from their execution. \n
- Load balancing — you can run multiple consumer threads for parallel processing. \n
- Data flow management — the queue smooths out peak loads, preventing system overload. \n
- Ease of testing — each component can be tested in isolation. \n
In Rust, the standard library provides the std::sync::mpsc module for creating channels with a single receiver. This is an ideal tool to get started.
Creating a Simple Queue with mpsc
\n\nLet's start with a basic example: one producer sends numbers, one consumer processes them.
\n\nuse std::sync::mpsc;\nuse std::thread;\nuse std::time::Duration;\n\nfn main() {\n // Create a channel: tx — sender, rx — receiver\n let (tx, rx) = mpsc::channel();\n\n // Start the producer thread\n thread::spawn(move || {\n for i in 1..=5 {\n println!("Sending: {}", i);\n tx.send(i).unwrap();\n thread::sleep(Duration::from_millis(500));\n }\n });\n\n // Consumer thread (main thread)\n for received in rx {\n println!("Received: {}", received);\n }\n}\n\n\nIn this example, tx (transmitter) can be cloned for multiple producers, while rx (receiver) is the single consumer. The send method returns a Result, which we handle with unwrap() — in real code, errors should be handled more gracefully.
Multiple Producers
\n\nTo create multiple producer threads, clone the sender:
\n\nuse std::sync::mpsc;\nuse std::thread;\n\nfn main() {\n let (tx, rx) = mpsc::channel();\n let tx1 = tx.clone();\n\n thread::spawn(move || {\n for i in 1..=3 {\n tx1.send(format!("Thread 1: message {}", i)).unwrap();\n }\n });\n\n thread::spawn(move || {\n for i in 1..=3 {\n tx.send(format!("Thread 2: message {}", i)).unwrap();\n }\n });\n\n for received in rx {\n println!("{}", received);\n }\n}\n\n\nNote: the original tx is moved into the second thread, so we clone it before the move.
Advanced Techniques: Task Types and Error Handling
\n\nIn real applications, tasks are rarely simple numbers. More often, they are structures containing data and logic. Let's look at how to organize a queue with different types of tasks.
\n\nEnum for Different Task Types
\n\nuse std::sync::mpsc;\nuse std::thread;\n\n#[derive(Debug)]\nenum Task {\n Compute { a: