Handling Resource Locks in Go: Using Mutex and Channels
In Go, if a goroutine tries to access a locked resource, then what happens depends on the locking mechanism being used.
Mutex Lock:
If a goroutine tries to access a resource protected by a sync.Mutex and that resource is already locked, the goroutine will be blocked and will wait until the lock becomes available. It will then proceed to acquire the lock and execute its critical section once the lock is released by the current holder.
Here is an example demonstrating this behavior using sync.Mutex:
package main
import (
"fmt"
"sync"
"time"
)
var (
lock sync.Mutex // Declaring a mutex lock
shared int // Shared resource
)
func main() {
wg := sync.WaitGroup{} // To wait for all goroutines to finish
wg.Add(2) // We are going to wait for two goroutines
go func() {
defer wg.Done()
lock.Lock() // Acquire the lock
fmt.Println("Goroutine 1: Acquired lock")
time.Sleep(2 * time.Second) // Simulate some processing
shared++
lock.Unlock() // Release the lock
fmt.Println("Goroutine 1: Released lock")
}()
go func() {
defer wg.Done()
time.Sleep(1 * time.Second) // Ensure this goroutine starts second
fmt.Println("Goroutine 2: Waiting to acquire lock")
lock.Lock() // Will block here until the lock is available
fmt.Println("Goroutine 2: Acquired lock")
shared++
lock.Unlock() // Release the lock
}()
wg.Wait() // Wait for both goroutines to finish
fmt.Println(shared) // Output will be 2
}
In this example:
- When Goroutine 2 tries to
lock.Lock(), it will block and wait because Goroutine 1 holds the lock. - Once Goroutine 1 releases the lock, Goroutine 2 will acquire it and proceed to execute its code.
Channel-based Lock:
If a goroutine tries to access a resource using a channel-based lock (like a semaphore), it will be blocked until it can successfully send to or receive from the channel (depending on the implementation).
Here's an example with a channel-based lock:
package main
import (
"fmt"
"time"
)
var (
lock = make(chan struct{}, 1) // Buffered channel for lock
shared int // Shared resource
)
func main() {
done := make(chan bool)
go func() {
lock <- struct{}{} // Acquire the lock
fmt.Println("Goroutine 1: Acquired lock")
time.Sleep(2 * time.Second) // Simulate some processing
shared++
<-lock // Release the lock
fmt.Println("Goroutine 1: Released lock")
done <- true
}()
go func() {
time.Sleep(1 * time.Second) // Ensure this goroutine starts second
fmt.Println("Goroutine 2: Waiting to acquire lock")
lock <- struct{}{} // Will block here until the lock is available
fmt.Println("Goroutine 2: Acquired lock")
shared++
<-lock // Release the lock
done <- true
}()
<-done // Wait for the first goroutine to finish
<-done // Wait for the second goroutine to finish
fmt.Println(shared) // Output will be 2
}
In this example:
- When Goroutine 2 tries to send
struct{}{}tolock, it will block and wait because the channel is full (indicating the lock is held by Goroutine 1). - Once Goroutine 1 reads from the channel and releases the lock, Goroutine 2 can then send
struct{}{}to the channel and proceed.
Summary:
- With
sync.Mutex: The waiting goroutine will block atlock.Lock()until the lock is released. - With channel-based lock: The waiting goroutine will block at the channel operation until it can successfully send to or receive from the channel.
In both scenarios, the waiting goroutine will be paused and won't proceed with its tasks until it can acquire the lock, ensuring safe access to the shared resource.