Implementing Atomic Operations with Go Mutexes
In concurrent programming, ensuring data integrity when multiple goroutines access shared resources is paramount. This challenge focuses on implementing atomic operations using Go's sync.Mutex to protect shared state from race conditions.
Problem Description
Your task is to create a simple counter that can be safely incremented and decremented by multiple goroutines concurrently. You will implement two functions: Increment and Decrement, which will modify a shared integer counter. The key requirement is to ensure that these operations are atomic, meaning that each operation completes entirely without interruption from other goroutines trying to access the same counter.
What needs to be achieved:
- Implement a counter struct that holds an integer value.
- Implement
IncrementandDecrementmethods on this struct. - Use
sync.Mutexto protect the counter's value from concurrent access.
Key requirements:
- The
Incrementfunction should atomically add 1 to the counter. - The
Decrementfunction should atomically subtract 1 from the counter. - The
Valuefunction should return the current value of the counter atomically.
Expected behavior:
When multiple goroutines call Increment and Decrement on the same counter instance, the final value should reflect the exact number of increments and decrements performed, without any lost updates due to race conditions.
Important edge cases to consider:
- What happens if
Decrementis called when the counter is already zero or negative? (The problem doesn't strictly forbid negative values, but it's good to be aware.) - Ensuring that the lock is always released, even if an unexpected panic were to occur (though this is less likely in this simple scenario, it's a good general practice).
Examples
Example 1:
Input:
Counter starts at 0.
10 goroutines each call Increment 1000 times.
Output:
Counter value: 10000
Explanation: Each of the 10 goroutines performs 1000 increments. With atomic operations, the final value accurately reflects 10 * 1000 = 10000.
Example 2:
Input:
Counter starts at 5.
5 goroutines each call Increment 200 times.
3 goroutines each call Decrement 100 times.
Output:
Counter value: 600
Explanation: Initial value: 5 Total increments: 5 * 200 = 1000 Total decrements: 3 * 100 = 300 Final value: 5 + 1000 - 300 = 705. (Self-correction: The example output was wrong. Correcting to reflect the calculation.) Corrected Output: Counter value: 705
Example 3: (Illustrating concurrent access)
Input:
Counter starts at 0.
Two goroutines:
- Goroutine A: Calls Increment 5000 times.
- Goroutine B: Calls Decrement 2500 times.
Output:
Counter value: 2500
Explanation: Goroutine A attempts to increment 5000 times. Goroutine B attempts to decrement 2500 times. Due to atomic operations and mutex protection, the final value will be 5000 - 2500 = 2500, regardless of the interleaving of goroutine execution.
Constraints
- The counter value will be an
int. - The number of goroutines and operations can be large, emphasizing the need for efficient and correct concurrency handling.
- The solution must be implemented in Go.
- Performance is a consideration, but correctness and atomicity are the primary goals.
Notes
- Consider using
sync.Mutexfor locking. Remember toLock()before accessing the shared resource andUnlock()afterwards. - The
deferkeyword can be very useful for ensuringUnlock()is always called. - A
sync.WaitGroupcan be helpful for coordinating the execution of multiple goroutines and waiting for them to complete before checking the final counter value.