-
Notifications
You must be signed in to change notification settings - Fork 18k
proposal: sync/atomic: Add Chan atomic channel #66960
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
|
I'm not sure whether maps would be useful, since the map itself is not thread safe - you'll need additional synchronization anyways. This is not about “completing” atomic types — there are also no atomic strings or functions — it is about improving something I see actively used: It would make WaitChans (see #16620) easier to implement. It's nothing you can't do now with |
You can use a map with atomic without other synchronization primitives, map is considered read only, changes are made via copying the entire map, but not sure whether this is worth adding. |
Lazy initialization of a channel can be done by var Ch = LazyChannel[int]() // Ch has type func() chan int
var F() {
Ch() <- 1
} where func LazyChannel[E any]() func() chan E {
return sync.OnceValue[chan E](func() chan E { return make(chan E) })
}
|
How does this work for zero values? |
Zero valued channels require an initialization step no matter what you do. With the |
It seems to me that meeting the "zero value is ready to use" goal requires the lazy channel to be represented entirely by a type (as opposed to an initialized function pointer value) so that it can be declared as a part of whatever wrapping type is trying to provide that guarantee: type UsesLazyChannel struct {
ch LazyChannel[string]
}
func (ulc *UsesLazyChannel) SendMessage(msg string) {
// ulc.ch.Ch() initializes the channel on first call, and returns
// the same channel for all calls thereafter.
ulc.ch.Ch() <- msg
}
//////////////////////////////////////////////
type LazyChannel[T] struct {
// ...
}
func (lc *LazyChannel[T]) Ch() chan T {
// ...
} The above (assuming there were some real implementation in place of those ulc := new(UsesLazyChannel)
ucl.Send("Hello") The most straightforward implementation of this type LazyChannel[T] struct {
ch chan T
once sync.Once
}
func (lc *LazyChannel[T]) Ch() chan T {
lc.once.Do(func () {
lc.ch = make(chan T)
})
return lc.ch
} Unlike the earlier proposed This is slightly more complex than the earlier proposed I don't see any great way to generalize this for buffered channels with Go's current features, because there's no current way to encode a desired buffered size as a type parameter. The best that comes to mind is to make The original proposal avoids that challenge by making it the caller's responsibility to actually |
Perhaps I didn’t explain my intention well enough: I know how to make lazy channels. Lazy channels are just a sample use case, and as far as this proposal is concerned only a motivation. Atomic channels are a low-level construct for library builders. I see atomic channels a lot: pkg context, gRPC-Go, HashiCorp Vault, 2, Benthos, and in 488 GitHub search results with duplicates. They all use So, this would improve existing code. It enables no new code, since everything that could be done with Given that atomic channels results in 488 matches, one could make a case for string (2.7k), slice (2.5k) or map (1.8k), but I assume that would be better discussed elsewhere. |
Thanks for the extra context, @eikemeier. FWIW when I was writing my variant of With the understanding that lazy channels is just one use-case (albeit apparently a major one, since that seems to be what many of those examples are doing), here's one more import "atomic"
type LazyChannel[T] struct {
ch atomic.Chan[T]
}
func (lc *LazyChannel[T]) Ch() chan T {
ret := lc.ch.Load()
if ret == nil {
return lc.init()
}
return ret
}
func (lc *LazyChannel[T]) init() chan T {
ch := make(chan T)
swapped := lc.ch.CompareAndSwap(nil, ch)
if swapped {
return ch
}
return lc.ch.Load()
} I've never personally had a need for "on-demand channel swapping", so I won't try for an example of that, but it could be useful to have a concrete example of a non-lazy-channel-related use-case to consider too. I agree that it does seem like this same argument could be made for all Go types that are secretly just pointers to a special data structure, or that involve pointers, to avoid the extra level of indirection. They seem justified in the same sense that While I do agree that those would be separate proposals, it does seem like (as you said) approving this proposal is likely to be justification for approving the others too, since the others seem to have even more existing examples. And now that I understand the proposal isn't just about lazy channels, I think offering all of these variations as low-level building blocks seems justified. |
Proposal Details
Add a generic Chan type, analog to other types:
Sample implementation in fillmore-labs.com/lazydone/atomic.
Motivation
Go has a “zero-value-is-useful” mindset, but structures containing channels need to initialize them lazily. It is a well-known problem for struct types to require non-nil channels. If you do not want to go through a mutex every time a channel is accessed (also, the mutex increases the structure size), candidates are
atomic.Value
oratomic.Pointer
.The first approach is used by context.WithCancel and requires casting:
The second approach by
lazydone.SafeLazy
requires some pointer arithmetic:Internally, a Go channel is a pointer to a
hchan
structure. So, the second approach uses a pointer to a pointer, which is wasteful.A channel (
*hchan
) can be cast from and to an unsafe.Pointer with:But nothing in the Go specification guarantees this.
The text was updated successfully, but these errors were encountered: