In concurrent programming within Go, the sync package does a lot of heavy lifting. Among its offerings, the sync.Cond type provides the building blocks necessary for crafting custom synchronization primitives safely. Let’s delve into using sync.Cond for achieving concurrent execution with signal-based synchronization.

Understanding sync.Cond

sync.Cond is a struct designed to manage goroutines with a condition. It ties together a mutex and a waiting condition, allowing goroutines to signal each other when state changes occur.

The basic operations for a sync.Cond are:

  • Wait(): A call to this method suspends execution of the calling goroutine’s case, releasing the associated lock.
  • Signal(): This method wakes up one of the goroutines waiting on the Cond (if there’s any).
  • Broadcast(): Invoked to wake all waiting goroutines.

Basic Example of sync.Cond

Below is a simple example illustrating how sync.Cond can be set up and used:


package main

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

var mu sync.Mutex
var cond = sync.NewCond(&mu)
var ready = false

func waitGoroutine() {
    cond.L.Lock()
    for !ready {
        cond.Wait()
    }
    fmt.Println("Goroutine: Received signal")
    cond.L.Unlock()
}

func signalFunction() {
    time.Sleep(2 * time.Second) // Simulate work
    cond.L.Lock()
    ready = true
    fmt.Println("Signaler: Sending signal")
    cond.Signal()
    cond.L.Unlock()
}

func main() {
    go waitGoroutine()
    signalFunction()
}

In this example, the waitGoroutine waits until the ready boolean becomes true. On reaching readiness, the signalFunction signals the change, allowing the waiting goroutine to resume.

Crafting Custom Synchronization

Beyond simple signaling, sync.Cond can help in creating advanced primitives. Consider a simple semaphore pattern:


package main

import (
    "fmt"
    "sync"
)

type Semaphore struct {
    v    int
    mtx  sync.Mutex
    cond *sync.Cond
}

func (s *Semaphore) Acquire() {
    s.mtx.Lock()
    for s.v <= 0 {
        s.cond.Wait()
    }
    s.v--
    s.mtx.Unlock()
}

func (s *Semaphore) Release() {
    s.mtx.Lock()
    s.v++
    s.cond.Signal()
    s.mtx.Unlock()
}

func NewSemaphore(initial int) *Semaphore {
    sem := Semaphore{v: initial}
    sem.cond = sync.NewCond(&sem.mtx)
    return &sem
}

func main() {
    sem := NewSemaphore(1)

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()
        sem.Acquire()
        fmt.Println("Goroutine 1: acquired the semaphore")
        sem.Release()
    }()

    go func() {
        defer wg.Done()
        sem.Acquire()
        fmt.Println("Goroutine 2: acquired the semaphore")
        sem.Release()
    }()

    wg.Wait()
}

Here, a semaphore is implemented with an adjustable initial value, allowing controlled access to a limited resource, demonstrating a real-world application of sync.Cond.

Conclusion

The sync.Cond type offers powerful capabilities for signaling and coordinating state among Go’s goroutines. By harnessing these features, you can create efficient, custom synchronization methods tailored for specific application needs.