Implementing a Go Mutex from Scratch
Concurrency is a fundamental aspect of modern software development. In Go, the sync.Mutex type is a crucial tool for managing shared resources and preventing race conditions. This challenge asks you to implement your own version of sync.Mutex from scratch, understanding the underlying mechanisms that make concurrent access safe.
Problem Description
Your task is to create a Go package that provides a Mutex type with Lock() and Unlock() methods. This custom Mutex should behave functionally like the standard library's sync.Mutex, ensuring that only one goroutine can hold the lock at any given time.
Key Requirements:
Lock()Method: This method should block until the mutex is available. If the mutex is already locked, the calling goroutine should wait. Once the mutex is acquired, it should remain locked untilUnlock()is called.Unlock()Method: This method releases the lock, allowing another waiting goroutine (if any) to acquire it. It must only be called by the goroutine that currently holds the lock. CallingUnlock()on an unlocked mutex will lead to undefined behavior (similar to the standardsync.Mutex).- Fairness (Optional but Recommended): While not strictly required for a basic implementation, a truly robust mutex would ideally be fair, meaning goroutines that have been waiting the longest get priority for acquiring the lock. For this challenge, a simple non-fair implementation is acceptable.
- Goroutine Safety: Your
Muteximplementation must be safe for concurrent access from multiple goroutines.
Expected Behavior:
When multiple goroutines attempt to Lock() a Mutex:
- Only one goroutine will successfully acquire the lock at a time.
- Other goroutines will block at the
Lock()call. - When the goroutine holding the lock calls
Unlock(), one of the blocked goroutines (if any) will unblock and acquire the lock.
Edge Cases:
- Calling
Unlock()when the mutex is not locked. - Multiple goroutines attempting to acquire the lock simultaneously.
- A goroutine attempting to
Lock()a mutex it already holds (this should ideally lead to deadlock, as persync.Mutexbehavior).
Examples
Example 1: Basic Locking and Unlocking
package main
import (
"fmt"
"sync"
"time"
)
// Assume your custom Mutex implementation is in a package named "mylock"
// import "your_module_path/mylock"
func main() {
var mu mylock.Mutex // Use your custom Mutex here
counter := 0
var wg sync.WaitGroup
numGoroutines := 1000
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
mu.Lock()
// Simulate some work
currentCounter := counter
time.Sleep(time.Millisecond) // Small sleep to increase chance of races
counter = currentCounter + 1
mu.Unlock()
}()
}
wg.Wait()
fmt.Printf("Final counter value: %d\n", counter)
}
Expected Output (approximately):
Final counter value: 1000
Explanation:
Without a mutex, the counter would likely be less than 1000 due to race conditions. The custom Mutex ensures that each goroutine increments the counter atomically, resulting in the correct final value.
Example 2: Demonstrating Blocking
package main
import (
"fmt"
"sync"
"time"
)
// Assume your custom Mutex implementation is in a package named "mylock"
// import "your_module_path/mylock"
func main() {
var mu mylock.Mutex // Use your custom Mutex here
go func() {
fmt.Println("Goroutine 1: Locking...")
mu.Lock()
fmt.Println("Goroutine 1: Acquired lock. Holding for 3 seconds.")
time.Sleep(3 * time.Second)
fmt.Println("Goroutine 1: Releasing lock.")
mu.Unlock()
}()
// Give Goroutine 1 a moment to acquire the lock
time.Sleep(100 * time.Millisecond)
fmt.Println("Main Goroutine: Trying to lock...")
mu.Lock() // This should block until Goroutine 1 unlocks
fmt.Println("Main Goroutine: Acquired lock.")
mu.Unlock()
fmt.Println("Main Goroutine: Unlocked.")
}
Expected Output:
Goroutine 1: Locking...
Goroutine 1: Acquired lock. Holding for 3 seconds.
Main Goroutine: Trying to lock...
Goroutine 1: Releasing lock.
Main Goroutine: Acquired lock.
Main Goroutine: Unlocked.
Explanation:
The main goroutine will attempt to acquire the lock, but it will block because Goroutine 1 already holds it. The main goroutine will only proceed after Goroutine 1 releases the lock.
Constraints
- Your
Muteximplementation must be thread-safe. - You cannot use
sync.Mutexor any other synchronization primitives from thesyncpackage (except forsync.WaitGroupfor testing purposes, as shown in examples). You may use lower-level primitives if necessary (e.g.,sync/atomic). - The implementation should be within a single Go package.
Notes
- Consider using
sync/atomicfor simple operations like checking and setting flags. - For blocking and waiting, you might need to leverage Go's channel mechanisms or explore more advanced synchronization primitives if you're aiming for a more sophisticated implementation.
- Think about the state your mutex needs to maintain (e.g., is it locked? who is waiting?).
- Deadlock is a common issue in concurrent programming. Ensure your
Lock()andUnlock()logic correctly handles scenarios to avoid or at least predictably handle deadlocks. - The standard
sync.Mutexis highly optimized. Your goal here is to understand the concepts rather than to achieve equivalent performance. - You will need to create a Go module for your
mylockpackage. A simple structure would be:
And thenyour_module_path/ └── mylock/ └── mutex.gogo mod init your_module_pathin the root directory.