Home

Concurrency Control: Locks and Channels in Go

24 views

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:

  1. Mutex Example:

    • sync.Mutex type is used to safely increment the shared counter.
    • lock.Lock() acquires the lock.
    • lock.Unlock() releases the lock.
  2. 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.