Hone logo
Hone
Problems

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 Executor should 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 + 'static closures.
  • 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 like Mutex and Arc for 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.
Loading editor...
rust