Using channel or sync.Cond to wait for condition
Asked Answered
C

2

6

I am trying to wait for a specific condition, and I would like advice as to how this is done best. I have a struct that looks like this (simplified):

type view struct {
    timeFrameReached bool
    Rows []*sitRow
}

In a goroutine, I am updating a file, which is read into the view variable. The number of rows increases, and timeFrameReached will ultimately be true.

Elsewhere, I want to wait for the following condition to be true:

view.timeFrameReached == true || len(view.Rows) >= numRows

I am trying to learn channels and how Go's condition variables work, and I would like to know what is the best solution here. Theoretically, I could do something trivial like this:

for {
    view = getView()
    if view.timeFrameReached == true || len(view.Rows) >= numRows {
        break
    }
}

but that is obviously a naive solution. The value of numRows comes from an HTTP request, so the condition method seems challenging. The goroutine would not know when to broadcast the condition because it wouldn't know the number of rows it is looking for.

Connote answered 19/1, 2017 at 21:2 Comment(0)
M
4

I think I would do this with a condition variable. The concept is not that a Signal should only be done when the condition the waiter wants to check is true, but when it might be true (i.e., the things being checked have changed).

The normal pattern for doing this is:

mutex.Lock()
for {
    view = getView()
    if view.timeFrameReached == true || len(view.Rows) >= numRows {
        break
    }
    cond.Wait(&mutex)
}
// Do stuff with view
mutex.Unlock()

Then, in the code where view is changed:

mutex.Lock()
// Change view
cond.Signal() // or cond.Broadcast()
mutex.Unlock()

Obviously, I've written this without knowing how your program works, so you may have to make some changes.

You could do something similar with a channel by sending on the channel to signal and trying to receive from the channel to wait, but that seems more complicated to me. (Also, if you have more than one goroutine waiting, you can signal all of them to wake up using cond.Broadcast.)

Morn answered 19/1, 2017 at 21:36 Comment(2)
That's a good idea. I have also posted an alternative idea that I came up with.Connote
Whilst this is a workable solution, experience shows that using mutexes is eventually unscalable: they may require detailed global knowledge that is hard to encapsulate. Goroutines and channels form composable elements (this is a core feature of CSP) and this is a reason to try to use them instead in cases such as this.Marcelline
C
1

One idea I have involves communicating the needed number of rows via a channel, and the goroutine that is building the view will do a non-blocking receive to see if the main thread is requesting a certain number of rows. If so, it will send a message back to indicate that the condition is met.

Here the main function requests a number of rows:

if numRows > len(viewFile.View.Rows) && !viewFile.View.TimeFrameReached {
    // Send the required number of rows
    rows <- numRows
    // Wait for the prefetch loop to signal that the view file is ready
    <-rows // Discard the response value and move on
    view = getView()
}

Here the goroutine checks if a certain number of rows are required. If so, it responds with an affirmative signal when ready. The value of that signal is inconsequential.

select {
    case numRows := <-rows:
        if len(viewFile.View.Rows) >= numRows || viewFile.View.TimeFrameReached {
            rows <- 1
        }
    default:
}
Connote answered 19/1, 2017 at 22:54 Comment(5)
You'd need two different channels for this, otherwise you don't know which piece of code is going to read numRows.Morn
Be careful about using both ends of the same channel within any goroutine. Unbuffered channels would usually lead to deadlock in that case. The first code snippet above has this problem I agree with Andy's comment above.Marcelline
An alternative is to use a single channel twice (as above), but changing the direction of send in the second instance. If it's an unbuffered channel, it will work for synchronisation either way around.Marcelline
@Rick-77 What do you mean by changing the direction of the send? I am using my above code, and it seems to be working. Here is also an example of this situation working: play.golang.org/p/iFj7PuUxLt. Can you help me understand your comments?Connote
The first case is rows <- numRows, writing to the channel. The second case is <-rows, reading from the channel. This could have been rows <- 0 or similar; the value doesn't matter when it's used only as a signalMarcelline

© 2022 - 2024 — McMap. All rights reserved.