Implementing Goroutine-Safe Initialization with sync.Once
In concurrent Go programs, it's common to encounter scenarios where a particular initialization logic needs to be executed exactly once, regardless of how many goroutines attempt to perform it. This is crucial for avoiding race conditions and ensuring data integrity. This challenge asks you to implement the core functionality of sync.Once.
Problem Description
Your task is to create a Go struct that mimics the behavior of sync.Once. This struct will be used to control the execution of a given function, ensuring it's called only one time, even when multiple goroutines call the method that triggers the function execution concurrently.
Key Requirements:
- Uniqueness of Execution: The function passed to your
Domethod must be executed exactly once. Subsequent calls toDowith the sameOnceinstance should not trigger the function again. - Goroutine Safety: Your implementation must be thread-safe. Multiple goroutines calling the
Domethod concurrently should not lead to race conditions or the function being executed more than once. - Blocking Behavior: If multiple goroutines call
Doconcurrently, all goroutines should wait until the first goroutine completes the execution of the provided function before returning from theirDocall.
Expected Behavior:
When Do(f func()) is called:
- If
fhas not been executed yet,Dowill executef. - If
fhas already been executed by any goroutine,Dowill return immediately without executingfagain. - If multiple goroutines call
Doconcurrently, only one goroutine will executef. The other goroutines will block untilfhas finished executing, and then they will also return.
Edge Cases to Consider:
- What happens if the function
fpanics? Your implementation should ideally handle this gracefully, though for this challenge, we'll assumefdoesn't panic to simplify the core synchronization logic. (In a realsync.Once, a panic would causeDoto re-panic and mark theOnceas "failed" for that execution attempt, but subsequent calls would still be safe from re-execution). - What if
Dois called with different functions by different goroutines? Each unique function would be executed based on the first call. However, thesync.Onceitself is a single entity, and its state tracks whether any function has been executed.
Examples
Example 1: Simple Sequential Execution
Let's say we have a function initializeConfig that we want to call exactly once.
package main
import (
"fmt"
"sync"
"time"
)
// Assume MyOnce is your implemented sync.Once struct
var myOnce MyOnce
var initialized bool
func initializeConfig() {
fmt.Println("Initializing configuration...")
time.Sleep(100 * time.Millisecond) // Simulate work
initialized = true
fmt.Println("Configuration initialized.")
}
func main() {
// Call initializeConfig for the first time
myOnce.Do(initializeConfig)
// Call initializeConfig again
myOnce.Do(initializeConfig)
fmt.Printf("Is config initialized? %v\n", initialized)
}
Expected Output:
Initializing configuration...
Configuration initialized.
Is config initialized? true
Explanation:
The initializeConfig function is called only once, even though myOnce.Do was invoked twice. The initialized variable correctly reflects that the configuration was set up.
Example 2: Concurrent Execution
This example demonstrates the goroutine-safe behavior.
package main
import (
"fmt"
"sync"
"time"
)
// Assume MyOnce is your implemented sync.Once struct
var myOnce MyOnce
var counter int
func incrementCounter() {
fmt.Println("Goroutine trying to increment counter...")
time.Sleep(50 * time.Millisecond) // Simulate some work
counter++
fmt.Printf("Counter incremented to: %d\n", counter)
}
func main() {
var wg sync.WaitGroup
numGoroutines := 5
wg.Add(numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
myOnce.Do(incrementCounter)
}()
}
wg.Wait()
fmt.Printf("Final counter value: %d\n", counter)
}
Expected Output (order of "Goroutine trying..." lines may vary):
Goroutine trying to increment counter...
Counter incremented to: 1
Goroutine trying to increment counter...
Goroutine trying to increment counter...
Goroutine trying to increment counter...
Goroutine trying to increment counter...
Final counter value: 1
Explanation:
All 5 goroutines call myOnce.Do(incrementCounter). Only one goroutine actually executes incrementCounter, and the counter is incremented only once. The other goroutines block and then return, seeing that the initialization has already occurred.
Constraints
- Your
MyOncestruct should not usesync.Onceinternally. You are to implement the logic from scratch. - You will likely need to use
sync.Mutexorsync.RWMutexto manage concurrent access to internal state. - Consider the state transitions your
Onceobject will go through. - The function
fpassed toDois assumed to be of typefunc().
Notes
- The core challenge is to ensure that the function provided to
Dois executed exactly once and that all goroutines callingDoeither wait for or have already observed this single execution. - Think about how to signal to waiting goroutines that the initialization is complete.
- Consider the initial state of your
MyOncestruct. - A common approach involves a boolean flag and a mutex. However, handling the blocking aspect for multiple concurrent callers is the trickiest part. You might need to explore additional synchronization primitives or patterns.