Home

Handling Resource Locks in Go: Using Mutex and Channels

33 views

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{}{} to lock, 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 at lock.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.