Implementing Write Barriers for Concurrent Data Structures
This challenge focuses on a critical aspect of concurrent programming: ensuring data consistency when multiple goroutines might be modifying shared data. You will implement a mechanism to prevent race conditions during writes to a shared resource by creating a "write barrier." This is essential for building robust and thread-safe data structures in Go.
Problem Description
You need to design and implement a WriteBarrier in Go that protects a shared []int slice. The WriteBarrier should allow multiple concurrent readers to access the slice safely, but only one goroutine should be able to write to the slice at any given time. Furthermore, any goroutine attempting to write must first acquire an exclusive lock, ensuring that no other goroutines (readers or writers) are accessing the slice during the write operation. Once the write is complete, the barrier should be released, allowing other operations to proceed.
Key Requirements:
- Concurrent Reads: Multiple goroutines should be able to read from the protected slice simultaneously.
- Exclusive Writes: Only one goroutine can write to the protected slice at a time.
- Write Blockage: While a write is in progress, all other read and write attempts must be blocked.
- Data Integrity: No reads should observe a partially updated state of the slice.
Expected Behavior:
- A
NewWriteBarrier(initialData []int)function should create and initialize aWriteBarrierwith the given data. - A
Read()method should return a copy of the current slice data. - A
Write(newData []int)method should update the slice data withnewDataafter ensuring exclusive access.
Edge Cases to Consider:
- Multiple goroutines attempting to read concurrently.
- Multiple goroutines attempting to write concurrently.
- A mix of read and write attempts occurring at the same time.
- What happens if
Writeis called withnilor an empty slice? (The barrier should handle this gracefully, potentially by setting the internal slice tonilor an empty slice).
Examples
Example 1:
// Assume a WriteBarrier is initialized with data: []int{1, 2, 3}
// Goroutine A calls Read()
// Goroutine B calls Read()
// Both A and B will receive a copy of []int{1, 2, 3} concurrently.
// The order in which they receive the data is not guaranteed, but they will both see the same consistent state.
Example 2:
// Assume a WriteBarrier is initialized with data: []int{1, 2, 3}
// Goroutine A calls Write([]int{4, 5})
// Goroutine B calls Read()
// The Write operation by A will acquire the exclusive lock.
// Goroutine B will block until A completes its write.
// Once A finishes, B will read the updated data: []int{4, 5}.
// The output of B.Read() will be []int{4, 5}.
Example 3:
// Assume a WriteBarrier is initialized with data: []int{1, 2, 3}
// Goroutine A calls Write([]int{4, 5})
// Goroutine B calls Write([]int{6, 7})
// Whichever goroutine acquires the write lock first (A or B) will perform its write.
// The other goroutine will block.
// For example, if A writes first, the data becomes []int{4, 5}. Then B will acquire the lock and write, making the data []int{6, 7}.
// If B writes first, the data becomes []int{6, 7}, then A writes, making it []int{4, 5}.
// The final state will depend on the exact scheduling, but only one write happens at a time, and no intermediate states are visible to other operations.
Constraints
- The underlying data structure to protect is a
[]int. - Your implementation must use Go's concurrency primitives (e.g.,
sync.RWMutex). - The
Read()method should return a copy of the data to prevent external modification of the internal slice. - The
Write()method should also ensure that thenewDatapassed in is copied before being assigned to the internal slice, if you intend to prevent external modification of the input slice after the write. However, the primary goal is to protect the internal slice state during concurrent access.
Notes
Consider using sync.RWMutex for its ability to allow multiple readers but only one writer. Think about how to ensure that writes are atomic and that reads always see a complete, consistent version of the data.
The challenge is to build a robust mechanism that prevents data races and ensures correct behavior in a highly concurrent environment. Success means creating a WriteBarrier that can be used safely by multiple goroutines for both reading and writing to the protected slice.