2
votes

I have an application where I am creating multiple goroutines to perform a certain task concurrently. All worker goroutines wait for a condition/event to occur and once the event is triggered, they begin their execution. After creating all goroutines, the main thread should know that all goroutines are indeed in waiting state before sending a broadcast signal.

I know this can be done using channels (which is something that is recommended) but I also found go's sync package interesting. Just trying to figure out how to achieve the same functionality using the sync package instead of channels

package main

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

var counter int

func worker(wg *sync.WaitGroup, cond *sync.Cond, id int) {
    fmt.Println("Starting Goroutine ID:", id)
    // Get a lock and wait
    cond.L.Lock()
    defer cond.L.Unlock()
    fmt.Println("Goroutine with ID: ", id, "obtained a lock")
    // Do some processing with the shared resource and wait
    counter++
    wg.Done()
    cond.Wait()
    fmt.Println("Goroutine ID:", id, "signalled. Continuing...")
}

func main() {
    var wg sync.WaitGroup
    cond := sync.NewCond(&sync.Mutex{})
    counter = 0
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go worker(&wg, cond, i)
    }

    wg.Wait() // Wait()'ing only until the counter is incremented
    // How to make sure that all goroutines you created are indeed wait()'ing ?????
    cond.Broadcast()
    time.Sleep(2 * time.Second)
    cond.Broadcast()
    fmt.Println("Final value of the counter is", counter)
}

If I don't use the below statements in the last 3 lines (except the fmt.Println)

    time.Sleep(2 * time.Second)
    cond.Broadcast()

I get the below output..

Starting Goroutine ID: 4
Goroutine with ID:  4 obtained a lock
Starting Goroutine ID: 3
Goroutine with ID:  3 obtained a lock
Starting Goroutine ID: 1
Goroutine with ID:  1 obtained a lock
Starting Goroutine ID: 0
Goroutine with ID:  0 obtained a lock
Starting Goroutine ID: 2
Goroutine with ID:  2 obtained a lock
Final value of the counter is 5
Goroutine ID: 3 signalled. Continuing...

Ideally every goroutine should be able to print

Goroutine ID: 3 signalled. Continuing...

with the corresponding goroutine id. We are not able to print that because not all goroutines are signalled as some of them are not even in the wait state. That is the reason I added time.Sleep which is not a practical solution.

My question is..How can I know that all goroutines are actually waiting on the condition cond.Wait()..Channels is a solution but I want to know how I could do this using go's sync package?

2

2 Answers

2
votes

You have a problem in that the Broadcast is not guaranteed to wake up all the goroutines since it only wakes up goroutines that are already waiting, and there's a small window between wg.Done() and cond.Wait(). Normally, a condition variable would be used with something that represents the "condition" that you're using for synchronization. In this case, you could have a bool package variable that indicates whether it's OK for the goroutines to continue. main would set that and then do a broadcast to tell the goroutines to continue. For example:

package main

    import (
    "fmt"
    "sync"
)

var counter int
var start bool

func worker(wg *sync.WaitGroup, cond *sync.Cond, id int) {
    fmt.Println("Starting Goroutine ID:", id)
    // Get a lock and wait
    cond.L.Lock()
    fmt.Println("Goroutine with ID: ", id, "obtained a lock")
    // Do some processing with the shared resource and wait
    counter++
    if !start {
        cond.Wait()
    }
    cond.L.Unlock()
    fmt.Println("Goroutine ID:", id, "signalled. Continuing...")
    wg.Done() // Worker is completely done
}

func main() {
    var wg sync.WaitGroup
    cond := sync.NewCond(&sync.Mutex{})
    counter = 0
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go worker(&wg, cond, i)
    }

    cond.L.Lock()
    start = true
    cond.Broadcast()
    cond.L.Unlock()

    wg.Wait() // Wait until all workers are done
    fmt.Println("Final value of the counter is", counter)
}

The addition of the start variable makes it impossible for the goroutines to not continue when main tells them to.

1
votes

All your goroutines did wake up. However, your program terminates before they can run. You don't need the second broadcast. It is sufficient if you simply wait after the first broadcast, so the goroutines can get a chance to run before the program terminates.