Building Basic Synchronization Primitives in Rust
Rust's standard library provides powerful synchronization primitives, but understanding how they work at a lower level is crucial for writing efficient and correct concurrent code. This challenge asks you to implement simplified versions of a mutex and a condition variable, giving you a deeper insight into the mechanics of thread synchronization. Successfully completing this challenge will solidify your understanding of shared mutable state and how to manage it safely in a concurrent environment.
Problem Description
You are tasked with implementing two fundamental synchronization primitives: a Mutex and a Condvar. These primitives are essential for managing access to shared mutable data in a concurrent setting, preventing race conditions and ensuring data integrity.
Mutex: The Mutex (Mutual Exclusion) should provide exclusive access to a protected resource. Only one thread can hold the lock at a time. Other threads attempting to acquire the lock will block until the lock is released.
Condvar: The Condvar (Condition Variable) allows threads to wait for a specific condition to become true. Threads can wait on a Condvar while releasing the associated Mutex. When another thread changes the condition and notifies the Condvar, waiting threads are awakened and can re-acquire the Mutex to check the condition again.
Key Requirements:
- Mutex:
lock(): Acquires the lock. Blocks if the lock is already held.unlock(): Releases the lock. Must be called by the thread that holds the lock.is_locked(): Returnstrueif the lock is currently held,falseotherwise.
- Condvar:
wait(mutex: &Mutex): Atomically releases theMutexand waits for a notification. Must be called while holding theMutex. Reacquires theMutexupon awakening.notify_one(): Awakens one waiting thread.notify_all(): Awakens all waiting threads.
Expected Behavior:
- Threads should block correctly when attempting to acquire a locked
Mutex. wait()should atomically release theMutexand suspend the thread.notify_one()andnotify_all()should correctly wake up waiting threads.- The
Mutexshould be reacquired by the thread afterwait()returns. - Error handling: While not strictly required for this simplified implementation, consider how you might handle errors like attempting to unlock a
Mutexthat isn't held.
Edge Cases to Consider:
- Spurious wakeups: Condition variables can sometimes wake up threads even when no notification has been sent. Your
wait()implementation should handle this by reacquiring the mutex and rechecking the condition. - Multiple threads waiting: Ensure that
notify_one()andnotify_all()correctly wake up the appropriate threads. - Nested waits: While not required, consider how your implementation would behave if a thread attempts to call
wait()while already waiting on theCondvar.
Examples
Example 1: Mutex Basic Locking
Input: Two threads, both attempting to lock a Mutex.
Output: Only one thread successfully acquires the lock at a time. The other thread blocks until the lock is released.
Explanation: Demonstrates the exclusive access provided by the Mutex.
Example 2: Condvar Wait and Notify
Input:
Thread 1: Holds a Mutex, calls wait() on a Condvar.
Thread 2: Holds the same Mutex, changes a condition, calls notify_one() on the Condvar.
Output: Thread 1 is awakened and reacquires the Mutex.
Explanation: Shows the basic functionality of waiting on a condition and being notified.
Example 3: Multiple Waiters
Input:
Three threads, all waiting on a Condvar.
Thread 4: Holds the Mutex, calls notify_all() on the Condvar.
Output: All three waiting threads are awakened.
Explanation: Demonstrates the behavior of notify_all().
Constraints
- No external crates: You are not allowed to use external crates like
std::sync. You must implement the primitives from scratch using only the standard library. - Atomicity: The atomic release and reacquisition of the mutex within
Condvar::waitis critical. Usestd::sync::atomic::Ordering::Releaseandstd::sync::atomic::Ordering::Acquirefor this. - Performance: While not the primary focus, avoid unnecessary overhead. Reasonable performance is expected.
- Safety: The code must be memory-safe and free from data races.
Notes
- Start by implementing the
Mutexfirst, as it's a building block for theCondvar. - Consider using an
AtomicBoolto track whether the mutex is locked. - The
Condvar's waiting mechanism will likely involve a queue or similar data structure to manage waiting threads. You can use aVecfor simplicity, but be mindful of potential performance implications with large numbers of waiting threads. - Spurious wakeups are a common issue with condition variables. Always recheck the condition after being awakened.
- Think carefully about the order of operations in
Condvar::wait()to ensure atomicity. The mutex release and thread suspension must happen as a single, indivisible operation. - This is a simplified implementation. Production-quality synchronization primitives are significantly more complex and optimized. The goal here is to understand the core concepts.