-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: Go 2: hierarchical select #52637
Comments
see also recent discussion: https://groups.google.com/g/golang-nuts/c/I9cbvCB86MA/m/GCKIJbzPAAAJ |
It seems to me that instead of select {
case x := <-c1:
...
case x := <-c2:
...
default:
select {
case x := <-c3:
...
default:
...
}
} |
I suppose that if you have a select {
case x := <-c1:
...
case x := <-c2:
...
default:
select {
case x := <-c3:
...
}
} If |
I too would like to see a deterministic select (vs random), at least as an option. Syntax, I'm not married to any particular ideas. Right now I have this, which .. is on the racey side. select {
case x := <-c1:
...
default:
select {
case x := <-c1:
...
case x := <-c2:
...
}
}
} If I don't have C1 immediately ready, and then both C1/C2 show up "simultaneously", it's random which one of C1/C2 will get the flow. And happens enough to make unit tests flakey. In reality, I'm actually doing 3 channels, with "best effort priority", but the code for that is really nasty. If this, then that. Otherwise, if those, do those. Otherwise, do all 3. With too much duplication, in a loop that's already expensive. I've also had to learn (and re-learn at least once more) the hard way that selects are non-deterministic. But that's on me, the docs are clear, even when the behavior is non-intuitive. |
It seems to me that the ordering of type PriorityChanQueue[T any] struct {
q pqueue[T]
out chan<- T
add chan item[T]
}
func NewPriorityChanQueue[T any](out chan<- T) *PriorityChanQueue[T] {
pcq := PriorityChanQueue[T]{
out: out,
add: make(chan item[T]),
}
go pcq.loop()
return &pcq
}
func (pcq PriorityChanQueue[T]) Stop() {
close(pcq.add)
}
func (pcq PriorityChanQueue[T]) Add(p int, v T) {
pcq.add <- item[T]{p: p, v: v}
}
func (pcq PriorityChanQueue[T]) C(p int, c <-chan T) {
go func() {
for v := range c {
pcq.add <- item[T]{p: p, v: v}
}
}()
}
func (pcq *PriorityChanQueue[T]) loop() {
defer close(pcq.out)
var out chan<- T
add := pcq.add
for {
select {
case out <- pcq.q.Peek():
pcq.q.Pop()
if len(pcq.q) == 0 {
if add == nil {
return
}
out = nil
}
case v, ok := <-add:
if !ok {
add = nil
continue
}
pcq.q.Push(item[T]{p: v.p, v: v.v})
out = pcq.out
}
}
} Maybe something like this, but with more thought put into it than this really quick example, could be added to |
I would think that in practice it is extremely unlikely that two different channels could be unblocked literally simultaneously. And even if by shear luck they did, you wouldn't be able to prove it anyway. Consequently, I feel like this aspect of a priority select is ill-defined. However, defining priority for the initial evaluation of the select cases, instead of simply randomly selecting among the available ones, is well-defined and very useful. As was mentioned, you can more or less get this behavior today with a bunch of select first {
case <-c1:
...
case <-c2:
...
case <-c3:
...
...
} |
I'm not sure about this. For example, what if you have something like for {
select {
case v := <-highPriority:
longRunningThing(v)
case v := <-lowPriority:
otherLongRunningThing(v)
}
} Then several things send to each while one of those long-running things is running. By the time the loop comes around again, even with unbuffered channels, you could easily have multiple things queued up in each channel. In that case, the I still think separate implementation is better, though. For example, if you have a |
@DeedleFake Yeah that's falls under the "initial evaluation" I was describing in the second paragraph. What I mean here is that once a |
If you actually need this level of complexity, then sure. But we generally don't. For example, it's fairly common for newcomers to write something like this: for {
select {
case <-ctx.Done():
return
case x := <- c:
// do something with x
}
} But then, there is non-determinism surrounding what happens when the context is canceled when the producer is "chatty". So then they have to rewrite it to this: for {
select {
case <-ctx.Done():
return
default:
}
select {
case <-ctx.Done():
return
case x := <- c:
// do something with x
}
} |
Here is an interesting example from Tailscale. This select statement wants to flush if none of the cases are true. This is done by repeating the entire select statement. https://github.com/tailscale/tailscale/blob/main/derp/derp_server.go#L1374 I don't think this proposal handles this case. Which is not a mark against it. But it would be interesting if we could handle this somehow. |
@griesemer points out that a better choice than |
Based on the discussion above, and the emoji voting, there isn't strong interest in this proposal. It is always possible to write the code that one wants, with some additional verbosity; this proposal just permits some code to be written somewhat more tersely. Therefore, this is a likely decline. Leaving open for four weeks for final comments. |
No further comments. |
Author background
Related proposals
Proposal
At the moment, when writing a
select
statement in Go I would write something like:where if both
c1
andc2
have pending entries, one of them will be selected uniformly at random.I quite often, however, found myself wishing to write some program logic that would require the
select
statement to prioritize consuming from a specific channel rather than another, if both are populated.As far as I am aware there is no mechanism in Go to do this. There are some workarounds that can be implemented, presented here but they all suffer from either not implementing the exact logic required, being very convoluted or being really inefficient (because they effectively end up acting as spin locks).
I know there are good reasons for having the nondeterministic choice be the default but I believe my proposal would be fully backwards compatible, preserving this behavior, while incorporating a new mechanism for prioritizing one or more channels.
My proposal would be the introduction of a new token that can go inside
select
statements (on the same level asdefault
). For the rest of this document, I will assume that this token is calledor:
but I have no strong feelings for what it should be called. This token will partition the select statement into blocks separated byor:
:If one or more channels from a given block has a pending value in it, it i guaranteed that the select statement will not choose to read from a channel in a succeeding block.
default:
will still work the same way, regardless of which block it belongs to (though it should be common practice to add it in the last one).This language feature would be fully backwards compatible since a statement with a single block will act like a normal select statement in current Go, but it will offer a very easy and intuitive way for people to work with hierarchical select statements and implement more complex and safe logic. The number of views on the Stack Exchange page that I liked earlier seems to suggest that there are a fair amount of people interested in a feature like this.
I don't have data to back this up, but I feel like there must be a fair amount of users trying to achieve this very thing using suboptimal solutions, so why not make it a language feature?
Costs
The text was updated successfully, but these errors were encountered: