Implement a Simple Executor in Rust
You are tasked with building a fundamental component of concurrent programming: an executor. An executor is responsible for taking tasks (often represented as futures or closures) and running them on a set of worker threads. This challenge will guide you through implementing a basic executor that can spawn and execute a given number of tasks concurrently.
Problem Description
Your goal is to create a Rust Executor struct that can accept and run asynchronous tasks. The executor should manage a pool of worker threads. When a task is submitted, the executor should assign it to an available worker thread for execution.
Key Requirements:
- Thread Pool: The
Executorshould maintain a pool of a fixed number of worker threads. - Task Submission: Provide a method to submit tasks (closures that return
()and can be sent across threads) to the executor. - Task Execution: Worker threads should pick up tasks from a shared queue and execute them.
- Shutdown: Implement a mechanism to gracefully shut down the executor, ensuring all submitted tasks complete (or are handled gracefully) before the threads exit.
Expected Behavior:
When tasks are submitted, they should be distributed among the worker threads. The executor should continue running tasks until explicitly shut down.
Edge Cases to Consider:
- What happens if more tasks are submitted than can be immediately processed?
- How do you handle the shutdown process to avoid deadlocks or lost tasks?
Examples
Example 1: Simple Task Execution
use std::thread;
use std::time::Duration;
// Assume an Executor struct is defined elsewhere with a `spawn` method
// let mut executor = Executor::new(2); // Create an executor with 2 threads
// Task 1: Print a message after a short delay
// let task1 = || {
// thread::sleep(Duration::from_millis(100));
// println!("Task 1 finished!");
// };
// executor.spawn(task1);
// Task 2: Print a different message
// let task2 = || {
// println!("Task 2 finished!");
// };
// executor.spawn(task2);
// Allow tasks to run
// thread::sleep(Duration::from_millis(200));
// Shutdown the executor
// executor.shutdown();
Expected Output (order might vary due to concurrency):
Task 2 finished!
Task 1 finished!
Explanation:
Two tasks are spawned. With an executor of 2 threads, both tasks can potentially run in parallel. Task 2 finishes quickly, while Task 1 has a small delay. The output demonstrates concurrent execution.
Example 2: Multiple Tasks on Limited Threads
use std::thread;
use std::time::Duration;
// Assume an Executor struct is defined elsewhere with a `spawn` method
// let mut executor = Executor::new(1); // Create an executor with 1 thread
// let mut tasks = Vec::new();
// for i in 0..5 {
// tasks.push(move || {
// thread::sleep(Duration::from_millis(50));
// println!("Task {} completed.", i);
// });
// }
// for task in tasks {
// executor.spawn(task);
// }
// Give tasks time to execute
// thread::sleep(Duration::from_millis(300));
// Shutdown the executor
// executor.shutdown();
Expected Output (order might vary, but tasks will complete sequentially due to 1 thread):
Task 0 completed.
Task 1 completed.
Task 2 completed.
Task 3 completed.
Task 4 completed.
Explanation:
With only one worker thread, the five tasks will be executed sequentially as the thread becomes available. Each task has a small delay, but the overall execution will be serialized.
Constraints
- The number of worker threads in the executor must be a positive integer.
- Tasks must be
Send + 'staticclosures. - The executor should handle up to a reasonable number of concurrently submitted tasks without panicking (e.g., you don't need to implement infinite buffering, but a basic channel-based approach is expected).
- The shutdown process should not block indefinitely if tasks are not completing.
Notes
- Consider using Rust's standard library features like
std::thread,std::sync::mpsc(for message passing between threads), and synchronization primitives likeMutexandArcfor managing shared state and communication. - A common pattern for executors is a task queue that worker threads poll from.
- For graceful shutdown, you might need a way to signal to the worker threads that no more tasks will be added and they should exit once their current task is done.
- Think about how you'll handle the case where a submitted task might panic. For this basic executor, you can allow panics to propagate and potentially terminate the worker thread (and the program). More advanced executors would handle this differently.