Concurrency Control: Locks and Channels in Go
In concurrent programming, "lock" and "unlock" mechanisms are used to control access to shared resources by multiple threads or goroutines, ensuring that data integrity is maintained and preventing race conditions.
Locks:
A lock ensures that only one goroutine or thread can access a critical section of code or a shared resource at a given time. When a goroutine acquires a lock, it must release the lock after it is done so that other goroutines can acquire it next.
Unlocks:
Unlock is the complementary operation to lock. After a goroutine finishes executing its critical section, it releases the lock so that other goroutines can acquire it.
Implementation in Go:
In Go, you can use the sync.Mutex type from the sync package for implementing locks and unlocks.
Here's a simple example:
package main
import (
"fmt"
"sync"
)
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() // Decrease the wait group counter when done
lock.Lock() // Acquire the lock
shared++
lock.Unlock() // Release the lock
}()
go func() {
defer wg.Done()
lock.Lock()
shared++
lock.Unlock()
}()
wg.Wait() // Wait for all goroutines to finish
fmt.Println(shared)
}
Using Channels for Locks:
You can also use channels as a lightweight way to implement locks. Here's an example using an empty struct channel, as mentioned earlier:
package main
import (
"fmt"
)
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
shared++
<-lock // Release the lock
done <- true
}()
go func() {
lock <- struct{}{}
shared++
<-lock
done <- true
}()
<-done // Wait for the first goroutine to finish
<-done // Wait for the second goroutine to finish
fmt.Println(shared)
}
Explanation:
-
Mutex Example:
sync.Mutextype is used to safely increment the shared counter.lock.Lock()acquires the lock.lock.Unlock()releases the lock.
-
Channel Example:
- A buffered channel of capacity 1 is used as a lock.
- Sending an empty struct to the channel acquires the lock.
- Receiving from the channel releases the lock.
Using these mechanisms ensures that the shared resource shared is accessed in a thread-safe manner, preventing race conditions.