Hone logo
Hone
Problems

Implementing a Go Mutex from Scratch

Concurrency is a fundamental aspect of modern software development. In Go, the sync.Mutex type is a crucial tool for managing shared resources and preventing race conditions. This challenge asks you to implement your own version of sync.Mutex from scratch, understanding the underlying mechanisms that make concurrent access safe.

Problem Description

Your task is to create a Go package that provides a Mutex type with Lock() and Unlock() methods. This custom Mutex should behave functionally like the standard library's sync.Mutex, ensuring that only one goroutine can hold the lock at any given time.

Key Requirements:

  • Lock() Method: This method should block until the mutex is available. If the mutex is already locked, the calling goroutine should wait. Once the mutex is acquired, it should remain locked until Unlock() is called.
  • Unlock() Method: This method releases the lock, allowing another waiting goroutine (if any) to acquire it. It must only be called by the goroutine that currently holds the lock. Calling Unlock() on an unlocked mutex will lead to undefined behavior (similar to the standard sync.Mutex).
  • Fairness (Optional but Recommended): While not strictly required for a basic implementation, a truly robust mutex would ideally be fair, meaning goroutines that have been waiting the longest get priority for acquiring the lock. For this challenge, a simple non-fair implementation is acceptable.
  • Goroutine Safety: Your Mutex implementation must be safe for concurrent access from multiple goroutines.

Expected Behavior:

When multiple goroutines attempt to Lock() a Mutex:

  • Only one goroutine will successfully acquire the lock at a time.
  • Other goroutines will block at the Lock() call.
  • When the goroutine holding the lock calls Unlock(), one of the blocked goroutines (if any) will unblock and acquire the lock.

Edge Cases:

  • Calling Unlock() when the mutex is not locked.
  • Multiple goroutines attempting to acquire the lock simultaneously.
  • A goroutine attempting to Lock() a mutex it already holds (this should ideally lead to deadlock, as per sync.Mutex behavior).

Examples

Example 1: Basic Locking and Unlocking

package main

import (
	"fmt"
	"sync"
	"time"
)

// Assume your custom Mutex implementation is in a package named "mylock"
// import "your_module_path/mylock"

func main() {
	var mu mylock.Mutex // Use your custom Mutex here
	counter := 0

	var wg sync.WaitGroup
	numGoroutines := 1000

	wg.Add(numGoroutines)
	for i := 0; i < numGoroutines; i++ {
		go func() {
			defer wg.Done()
			mu.Lock()
			// Simulate some work
			currentCounter := counter
			time.Sleep(time.Millisecond) // Small sleep to increase chance of races
			counter = currentCounter + 1
			mu.Unlock()
		}()
	}

	wg.Wait()
	fmt.Printf("Final counter value: %d\n", counter)
}

Expected Output (approximately):

Final counter value: 1000

Explanation:

Without a mutex, the counter would likely be less than 1000 due to race conditions. The custom Mutex ensures that each goroutine increments the counter atomically, resulting in the correct final value.

Example 2: Demonstrating Blocking

package main

import (
	"fmt"
	"sync"
	"time"
)

// Assume your custom Mutex implementation is in a package named "mylock"
// import "your_module_path/mylock"

func main() {
	var mu mylock.Mutex // Use your custom Mutex here

	go func() {
		fmt.Println("Goroutine 1: Locking...")
		mu.Lock()
		fmt.Println("Goroutine 1: Acquired lock. Holding for 3 seconds.")
		time.Sleep(3 * time.Second)
		fmt.Println("Goroutine 1: Releasing lock.")
		mu.Unlock()
	}()

	// Give Goroutine 1 a moment to acquire the lock
	time.Sleep(100 * time.Millisecond)

	fmt.Println("Main Goroutine: Trying to lock...")
	mu.Lock() // This should block until Goroutine 1 unlocks
	fmt.Println("Main Goroutine: Acquired lock.")
	mu.Unlock()
	fmt.Println("Main Goroutine: Unlocked.")
}

Expected Output:

Goroutine 1: Locking...
Goroutine 1: Acquired lock. Holding for 3 seconds.
Main Goroutine: Trying to lock...
Goroutine 1: Releasing lock.
Main Goroutine: Acquired lock.
Main Goroutine: Unlocked.

Explanation:

The main goroutine will attempt to acquire the lock, but it will block because Goroutine 1 already holds it. The main goroutine will only proceed after Goroutine 1 releases the lock.

Constraints

  • Your Mutex implementation must be thread-safe.
  • You cannot use sync.Mutex or any other synchronization primitives from the sync package (except for sync.WaitGroup for testing purposes, as shown in examples). You may use lower-level primitives if necessary (e.g., sync/atomic).
  • The implementation should be within a single Go package.

Notes

  • Consider using sync/atomic for simple operations like checking and setting flags.
  • For blocking and waiting, you might need to leverage Go's channel mechanisms or explore more advanced synchronization primitives if you're aiming for a more sophisticated implementation.
  • Think about the state your mutex needs to maintain (e.g., is it locked? who is waiting?).
  • Deadlock is a common issue in concurrent programming. Ensure your Lock() and Unlock() logic correctly handles scenarios to avoid or at least predictably handle deadlocks.
  • The standard sync.Mutex is highly optimized. Your goal here is to understand the concepts rather than to achieve equivalent performance.
  • You will need to create a Go module for your mylock package. A simple structure would be:
    your_module_path/
    └── mylock/
        └── mutex.go
    
    And then go mod init your_module_path in the root directory.
Loading editor...
go