Hone logo
Hone
Problems

Go Memory Model: Concurrent Data Access

This challenge focuses on understanding and implementing safe concurrent data access in Go. You will build a simple in-memory key-value store that can be accessed by multiple goroutines simultaneously without causing data corruption or race conditions. This is a fundamental concept for building robust Go applications.

Problem Description

Your task is to create a thread-safe in-memory key-value store. This store should support basic operations like setting a value for a given key, getting a value associated with a key, and deleting a key-value pair. The critical requirement is that these operations must be safe to call concurrently from multiple goroutines.

Key Requirements:

  • Set(key string, value interface{}): Adds or updates a key-value pair in the store. value can be of any type.
  • Get(key string) (interface{}, bool): Retrieves the value associated with a key. Returns the value and a boolean indicating whether the key was found.
  • Delete(key string): Removes a key-value pair from the store. If the key does not exist, this operation should do nothing.
  • Thread Safety: All operations (Set, Get, Delete) must be safe to call concurrently from multiple goroutines without leading to data races.

Expected Behavior:

When multiple goroutines call Set, Get, and Delete on the same MemoryStore instance, the data within the store should remain consistent. For example, if one goroutine sets a value and another immediately tries to get it, the second goroutine should see the updated value, not a stale one. Similarly, deletions should be atomic.

Edge Cases:

  • Accessing a non-existent key with Get.
  • Deleting a non-existent key.
  • Concurrent Set operations on the same key.
  • Concurrent Get and Set operations on the same key.
  • Concurrent Delete and Get operations on the same key.

Examples

Example 1: Basic Operations

Input:
store := NewMemoryStore()
go store.Set("name", "Alice")
go store.Set("age", 30)
time.Sleep(100 * time.Millisecond) // Give goroutines time to execute

output1, found1 := store.Get("name")
output2, found2 := store.Get("age")
output3, found3 := store.Get("city")

Output:
output1: "Alice", found1: true
output2: 30, found2: true
output3: nil, found3: false
Explanation: The store successfully stored and retrieved string and integer values. The non-existent key "city" was correctly reported as not found.

Example 2: Concurrent Updates and Deletion

Input:
store := NewMemoryStore()
var wg sync.WaitGroup

for i := 0; i < 100; i++ {
    wg.Add(1)
    go func(k int) {
        defer wg.Done()
        key := fmt.Sprintf("key_%d", k)
        store.Set(key, k)
    }(i)
}
wg.Wait() // Ensure all sets are done

// Verify some values
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(k int) {
        defer wg.Done()
        key := fmt.Sprintf("key_%d", k)
        val, found := store.Get(key)
        if !found || val.(int) != k {
            // This should ideally not happen in a correct implementation
            fmt.Printf("Error: Expected %d for %s, got %v, found %t\n", k, key, val, found)
        }
    }(i)
}
wg.Wait()

// Delete some keys concurrently
for i := 0; i < 50; i++ {
    wg.Add(1)
    go func(k int) {
        defer wg.Done()
        key := fmt.Sprintf("key_%d", k)
        store.Delete(key)
    }(i)
}
wg.Wait()

// Check a deleted key and a remaining key
val1, found1 := store.Get("key_5") // Should be deleted
val2, found2 := store.Get("key_75") // Should still exist

Output:
(Potentially some error messages if verification fails, but ideally none)
val1: nil, found1: false
val2: 75, found2: true
Explanation: Goroutines concurrently set 100 keys. Some keys were then concurrently deleted. The store correctly reflects that deleted keys are no longer present, while remaining keys retain their values.

Constraints

  • The MemoryStore must be implemented using standard Go library features. No external dependencies are allowed.
  • The MemoryStore should handle an arbitrary number of keys and values.
  • Performance should be reasonable for typical concurrent access patterns, but extreme micro-optimizations are not the primary focus. The correctness of concurrency is paramount.

Notes

  • Consider how to protect the underlying data structure from concurrent modification.
  • Think about which synchronization primitives in Go are most suitable for this task.
  • The interface{} type allows storing values of any Go type, making the store versatile.
  • A common approach involves using a map to store the key-value pairs, but the map itself is not inherently thread-safe.
Loading editor...
go