-
Notifications
You must be signed in to change notification settings - Fork 18k
proposal: new channel primitive, drain(c) #26343
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
Comments
You haven't done anything to convince anyone that this is a good thing to have. All you have done is describe the mechanics of drain. Please describe some use cases and explain why having drain enables new functionality or why not having it leads to difficult code today. If this was already written in the other thread, please copy into your above post. |
It arose from a discussion about primitives for channel operations that avoided creating goroutines just to shuffle data around. The splice and clone operations from the original issue are more general and more generally useful. drain would allow "undoing" a clone that you are no longer interested in efficiently. Its use is a single reader that is no longer interested in what the writer has to say:
Without clone that's less compelling, just more concise and efficient than creating a goroutine whose only purpose is to discard. If it were more like the reverse of close and the writer could test whether the reader is still responding, it would have greater use on its own. In the example above the goroutine writing to c would be able to tell that c is being ignored and shutdown, without having to close a separate shutdown channel. That could simplify a lot of single-reader code. It would mean making the send statement into an expression returning a boolean, which is a greater ask than a new builtin. |
Can't this be handled automatically by the garbage collector, especially as a corollary to |
@DeedleFake Let's say the original channel is |
Hmmm... So, let's say I do the following: c := make(chan int)
cc := clone(c) There'll presumably be three places these are then going to be used:
The issue you're worried about is that, because of the buffering, if the reading reference to
I just don't like the idea of requiring a manual |
Reading from If you leave it up to gc that means that any reader can be dropped without changing behavior as long as at least one is still being read from. If there are 99 clones of a channel that means you could safely let any 99 of the 100 readers go, but if you let that random last reader go suddenly things behave differently. It's very fragile. |
Right, but the problem would be that the garbage collector can't tell how the original two-way reference is used. If I have something running that writes to it, there's no way to be sure that some other code path won't start reading from it, too. Because of that, that original reference has to be buffered the same way that the clones are, but, unlike the clones, if that reference is dropped from the code that's reading from it, the buffer can't be freed because there's still a reference to it from the writing end, meaning that every write to it will just grow an unfreeable buffer, resulting in a memory leak every single time For example: c := make(chan int)
go func() {
for i := 0; true; i++ {
c <- i
}
}()
go func() {
for i := range(c) {
fmt.Printf("Original: %v\n", i)
if i > 5 {
break
}
}
// At this point, no more code is reading from the original c, but
// there's no way for the garbage collector to know that because the
// reference is still in scope elsewhere, so the buffer has to
// continuously grow in case something decides to read from it.
}()
go func() {
cc := clone(c)
for i := range(cc) {
fmt.Printf("Clone: %v\n", i)
if i > 5 {
break
}
}
// At this point, the only reference to cc goes out of scope, so the
// garbage collector can clean up its buffer.
}() |
I would not name that operator “ But, honestly, I'm worried that this primitive would encourage programs to perform a lot of needless work. With a cancellation channel, the sender goroutine stops being able to send, and knows to avoid computing any future results. With a “drained” channel, the sender gets no such signal and potentially wastes a lot of CPU computing results that nobody wants. At the very least, I would want to see some more compelling examples of where this would be useful. |
The purpose of this function seems to be to permit a goroutine to write a series of values to a channel without having to worry whether any other goroutine is still reading from it. But you still need some mechanism to stop the goroutine. Otherwise, calling Since there needs to be some way to stop the goroutine anyhow, when you stop the goroutine, the channel will garbage collected. The only purpose for |
Forked from #26282.
drain(c)
would be roughly equivalent to the current idiom ofUnlike the above code,
drain(c)
does not create a new goroutine. Sends toc
are discarded by the scheduler.After a call to
drain(c)
, another attempt to receive fromc
panics.This is similar to
close(c)
but for readers. It would only be safe to usedrain
when there was a single reader the same way it's only safe toclose
a chan when there's a single writer.Open questions:
Would select need to prioritize non-drained channels?
Should it be possible for a sender to detect when a channel has been drained? Doing so would allow a sender to shutdown, but as @neild pointed out "Channels currently pass data in only one direction. Being able to detect a drained channel would make that bidirectional."
The text was updated successfully, but these errors were encountered: