Hone logo
Hone
Problems

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:

  1. Uniqueness of Execution: The function passed to your Do method must be executed exactly once. Subsequent calls to Do with the same Once instance should not trigger the function again.
  2. Goroutine Safety: Your implementation must be thread-safe. Multiple goroutines calling the Do method concurrently should not lead to race conditions or the function being executed more than once.
  3. Blocking Behavior: If multiple goroutines call Do concurrently, all goroutines should wait until the first goroutine completes the execution of the provided function before returning from their Do call.

Expected Behavior:

When Do(f func()) is called:

  • If f has not been executed yet, Do will execute f.
  • If f has already been executed by any goroutine, Do will return immediately without executing f again.
  • If multiple goroutines call Do concurrently, only one goroutine will execute f. The other goroutines will block until f has finished executing, and then they will also return.

Edge Cases to Consider:

  • What happens if the function f panics? Your implementation should ideally handle this gracefully, though for this challenge, we'll assume f doesn't panic to simplify the core synchronization logic. (In a real sync.Once, a panic would cause Do to re-panic and mark the Once as "failed" for that execution attempt, but subsequent calls would still be safe from re-execution).
  • What if Do is called with different functions by different goroutines? Each unique function would be executed based on the first call. However, the sync.Once itself 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 MyOnce struct should not use sync.Once internally. You are to implement the logic from scratch.
  • You will likely need to use sync.Mutex or sync.RWMutex to manage concurrent access to internal state.
  • Consider the state transitions your Once object will go through.
  • The function f passed to Do is assumed to be of type func().

Notes

  • The core challenge is to ensure that the function provided to Do is executed exactly once and that all goroutines calling Do either 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 MyOnce struct.
  • 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.
Loading editor...
go