Hone logo
Hone
Problems

Implement a Bounded Thread Pool in Rust

Modern applications often need to perform tasks concurrently to improve performance and responsiveness. A thread pool is a common pattern for managing a set of worker threads that can execute tasks. This challenge asks you to implement a basic, bounded thread pool in Rust, capable of accepting tasks and distributing them among a fixed number of worker threads.

Problem Description

You need to create a ThreadPool struct in Rust that manages a fixed number of worker threads. The thread pool should be able to:

  1. Initialize: Be created with a specified number of worker threads.
  2. Accept Jobs: Provide a method to submit new tasks (closures) to the pool.
  3. Execute Jobs: Distribute the submitted tasks among the worker threads.
  4. Graceful Shutdown: Allow for the thread pool to be shut down, ensuring all submitted tasks are completed before exiting.

Key Requirements:

  • The thread pool should have a fixed size, meaning the number of worker threads is determined at creation time and does not change.
  • Tasks submitted to the pool must be executable closures that take no arguments and return () (unit).
  • The pool should utilize Rust's std::thread for creating worker threads.
  • Synchronization mechanisms (like channels or mutexes) will be necessary to safely pass tasks from the main thread to worker threads and to manage shared state.
  • The ThreadPool should implement the Drop trait to ensure a clean shutdown.

Expected Behavior:

When a task is submitted, it should be placed in a queue. Worker threads will pick up tasks from this queue and execute them. If the queue is empty, worker threads should wait. Upon shutdown, all worker threads should finish their current task, process any remaining tasks in the queue, and then terminate.

Edge Cases to Consider:

  • Submitting tasks when the pool is shutting down.
  • Creating a thread pool with zero threads.
  • Handling potential panics within executed closures.

Examples

Example 1: Basic Task Execution

// Assume a ThreadPool is created with 2 threads
let mut pool = ThreadPool::new(2);

// Submit a few tasks
pool.execute(|| {
    println!("Task 1 running on thread {:?}", std::thread::current().id());
    std::thread::sleep(std::time::Duration::from_millis(100));
});
pool.execute(|| {
    println!("Task 2 running on thread {:?}", std::thread::current().id());
    std::thread::sleep(std::time::Duration::from_millis(100));
});
pool.execute(|| {
    println!("Task 3 running on thread {:?}", std::thread::current().id());
    std::thread::sleep(std::time::Duration::from_millis(100));
});

// The program will wait for all tasks to complete upon exiting the scope
// or if a join/shutdown method is explicitly called.

Expected Output (order of task execution and thread assignment may vary):

Task 1 running on thread ThreadId(X)
Task 2 running on thread ThreadId(Y)
Task 3 running on thread ThreadId(X) // or ThreadId(Y)

Explanation:

Three tasks are submitted to a pool of two threads. The threads pick up tasks as they become available. For instance, Thread X might take Task 1, Thread Y takes Task 2. Once Thread X finishes Task 1, it can pick up Task 3.

Example 2: Shutdown Behavior

// Assume a ThreadPool is created with 2 threads
let mut pool = ThreadPool::new(2);

// Submit a task
pool.execute(|| {
    println!("First task starts...");
    std::thread::sleep(std::time::Duration::from_secs(2));
    println!("First task finishes.");
});

// Submit another task
pool.execute(|| {
    println!("Second task starts...");
    std::thread::sleep(std::time::Duration::from_secs(1));
    println!("Second task finishes.");
});

// Explicitly drop the pool (or let it go out of scope)
drop(pool); // This should block until all tasks are done.

println!("ThreadPool dropped. All tasks completed.");

Expected Output:

First task starts...
Second task starts...
Second task finishes.
First task finishes.
ThreadPool dropped. All tasks completed.

Explanation:

Even though drop(pool) is called, the Drop implementation for ThreadPool should ensure that both tasks are completed before the program continues and prints "ThreadPool dropped. All tasks completed.".

Constraints

  • The number of worker threads must be a positive integer.
  • Tasks submitted to the pool must be FnOnce() + Send + 'static. This means they are closures that can be called once, are safe to send between threads, and can live for the entire duration of the program.
  • The thread pool implementation should avoid deadlocks.
  • Performance: While not strictly benchmarked, the implementation should be reasonably efficient and not introduce unnecessary bottlenecks.

Notes

  • Consider using std::sync::mpsc::channel for communication between the main thread (submitting jobs) and the worker threads (executing jobs).
  • You will need a way to signal to the worker threads that no more jobs will be submitted and that they should shut down after finishing their current work.
  • std::sync::Arc might be useful for sharing data (like the job sender) between multiple worker threads.
  • Think carefully about how to handle the lifetime of closures and any data they capture. The 'static bound is crucial here.
  • Implementing Drop correctly is key to ensuring the thread pool cleans up after itself.
Loading editor...
rust