-
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: sync: remove the Cond type #21165
Comments
The main thing that cc @bradfitz since he decided to use |
I don't use sync.Cond often, but when I have, I've been happy it exists. I agree that it's hard to use in other languages where you only have, say, locks and condition variables as concurrency primitives. But in Go, where we have channels, one can reserve use of sync.Cond for those specific cases where it's a good fit. Besides the signal+broadcast case mentioned, another case where it's not obvious to me how to replace a Cond with a channel is where the mutex linked to the Cond protects some state. I typically want to lock, modify some state, and then cond.Wait -- which releases the lock. If I try to replace the Cond with a channel but keep the mutex, now I have two independent synchronization primitives, and that seems trickier to reason about. |
Indeed, that's the trickier type of usage I've seen in practice. (It's also, in my experience, the type more likely to have deadlocks, or spurious or missed signals.) That kind of usage is illustrated in I know of at least two techniques that can apply for that type of usage.
(For the latter technique, there may be something we could factor out into a library that would harmonize with the rest of Go better than |
|
Agreed, but is pthread really a good example for Go's concurrency model to emulate? |
I think Go2 could improve Cond but I think removing Cond is kinda crazy. Using channels is currently a super heavy replacement. |
Note that a single Cond lets multiple waiters each wait on a slightly different condition, such as an incrementing counter reaching various thresholds. I mentioned a real-life example of this in #16620 (comment). Channels seem to be a poor fit for this. |
Channels cannot be reopened to broadcast twice. |
Since x/net/http2 was mentioned: I recently wrote a CL for x/net/http2 that was complicated by the use of Cond, because I wanted to select on set of channels or a Cond. I had to implement this by spinning a goroutine to wait on the channels, then broadcast to the Cond if one of those channels fired. And FWIW, x/net/http2 uses Broadcast exclusively, never Signal. I don't have a position on removing Cond, per se, but more than once I have been annoyed by not being able to select on a Cond and a channel simultaneously. If that could be fixed I would be happy. |
I find channels easier to use than sync.Cond, but channels don't provide for efficient, repeated broadcasting of more than one bit from 1:N receivers. I suggest adding a new type of channel, a broadcast channel. It would be very similar to a channel with buffer size 1, with some extra logic: a) if the channel has a value, then receiving on a broadcast channel doesn't consume the value in it. This allows an unlimited number of receivers to receive the same value. b) delete on the broadcast channel removes any value in it. This allows the sender to stop broadcasting any value at all. Notes
Alternatives: A2) a more general approach would be to allow the sending of values tagged as "sticky", and let sticky values be consumed an infinite number of times. This could be nice if you want the buffer of the channel to contain both sticky and non-sticky values. You would need a new operation to consume and eliminate a sticky value. A3) allow the size of the buffer attached to a channel to be resized without being re-allocated. Then, assuming you know how many subscribers you have, the sender can send exactly that many values on a regular buffered channel. Unfortunately this requires that the sender have "register a new client" logic so it knows when to expand the queue. Corresponding de-registration logic required as well. Hence I prefer the broadcast type channels suggested first. edit: A2 may have the advantage of being the most backwards compatible, and the advantage of having obviously novel syntax for sending sticky values. Suppose for instance that the operator |
Over in #16620 (comment) I described taking some of my sync.Cond-using code and altering it to use a channel instead. I found it to be fairly tricky to get right (though of course it's possible that I overlooked some simpler way). |
This couldn't be more true, I would gladly take performance loss in favor of debugging someone else's condition variable usage. People see it in stdlib, copy it, get it wrong, and then spend late nights debugging it. sync.Cond is a keyword in my bug comb |
Change https://golang.org/cl/94138 mentions this issue: |
First I'll note that Go 2 should be largely if not entirely backward compatible with Go 1. We could remove it from a new version of the sync package, whether that is called "sync/v2" or something else. But I don't think that removing condition variables is a sufficient reason to create a new version of the sync package. |
I have used sync.Cond in a number of situations. Channels are really heavy weight, and its significant that a number of constructs that would be easy to implement as a relatively straight-forward condition variable followed by checks of several conditions can be implemented in terms of select {}, but at relatively large performance costs. sync.Cond is harder to use than channels, and possibly more error prone, but for highly performance sensitive code there are so many constructs that are vastly more efficient with sync.Cond than select {} multiple conditions that removing sync.Cond would be devastating for projects that I work on. Removing this construct would be devastating to me, and would be a reason to abandon to the language entirely for certain of my projects. |
Let me be clear here. If an implementation using channels, and multiple channels, can be show to be roughly equivalent in terms of performance with a using a single condition variable followed by multiple if {} statements, then I'd be willing to accept that channels are an equivalent replacement. My own experience is that this is vehemently not the case, and I believe that this proposal stems from a naive understanding of the documented semantics of channels and condition variables, without sufficient experience in using either in performance critical code to make assertions about the lack of utility of one vs. the other. |
It is true that a channel is currently larger than a sync.Cond, and requires more allocations. However, I don't believe that that is inherent to the use of channels. (#28366 describes one option to streamline both the API and implementation.)
Can you provide some benchmarks to demonstrate that? I'd be curious to see how well the channel alternatives can be optimized. In my experience, even today the performance cost of untargeted wakeups can often swamp out the other performance advantages of condition variables. |
That's fair enough. If the benefit of removing We had a pretty clear education gap between use of |
@bcmills What is your beef here? Why do you so want to take away a tool that has proven itself quite useful many many times over? I know that sync.Cond() has resulted in cleaner and simpler code over attempts to make this work with channels, and faster code too! in many of my projects. Yes, there are sharp edges here, but these are hardly the only such. They are no worse than sync.Mutex in terms of dangerous tooling. |
Experience report on
In #21165 (comment) it turned out trivial to replace the |
@kevinburke The concept of sharing variable with mutex is once 1 of the goroutine acquired the Lock(), the rest of 99 goroutine have to wait until the goroutine that previously acquired the Lock() to Unlock(). The concept of Wait() is :
I'd run your code multiple times, sometimes it is PASS and sometimes it FAIL. It's not always FAIL. If you add time.Sleep after wg.Wait() , you will see that the Wait() is running.
|
Change https://go.dev/cl/412237 mentions this issue: |
If |
Cond is difficult to use correctly (I was just bitten by it in a production app that I inherited). While several proposals have come up to improve or remove sync.Cond, no action has so far been taken. Update the documentation to discourage use of sync.Cond, and point people in the direction of preferred alternatives. I believe this will help encourage behavior we want (less use of sync.Cond and more use of channels), while also paving the way for, potentially, removing Cond in a future version of the language. Thanks very much to Bryan Mills and Sean Liao for discussion and recommendations. Updates #20491. Updates #21165. Change-Id: Ib4d0631c79d4c4d0a30027255cd43bc47cddebd3 Reviewed-on: https://go-review.googlesource.com/c/go/+/412237 Run-TryBot: Ian Lance Taylor <iant@google.com> Auto-Submit: Ian Lance Taylor <iant@google.com> Reviewed-by: Bryan Mills <bcmills@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Ian Lance Taylor <iant@google.com>
I think there are uses cases that do require sync.Cond and sync.Cond makes the code much smaller and easier to understand once you know how sync.Cond works. I wanted a Golang stream multiplexer, and after writing my own using channels, I found sync.Cond from https://github.com/SiaFoundation/mux and forked that project into https://github.com/Acebond/gomux. The gomux project is like 1/3 the code of my messy channels version and performs 2x as fast. Using sync.Cond is the only way to achieve optimal throughput in a stream multiplexer. |
It would be a disaster for us if Cond removed from language. We are writing highly loaded services in golang using bunch of highly optimized components, like custom API RPC server. We process million+ requests per second with almost no allocations and maintaining fixed memory ceiling, or in other words regardless of load server uses predefined amount of memory and never more. If requests come faster than they can be processed, they are not read from their sockets until enough memory is released. We tried to use channels first when designed our components, but gradually got rid of almost of them, as a result removing most allocations, simplifying invariants and greatly speeding code up. Problem with channels for us is they are hard to use with cancellation semantic and they have data transfer and signalling mixed. Most APIs with channels are hard to use and lead to various races which cannot be easily solved (good example is standard Timer). Often we need to make operation on channel and external variables atomic, because channel content forms invariant with those. Also we often want explicit clear or peek operations. Mutexes and Conds plus normal variables always give us easy inspection, manipulation and cancellation. When we need some state machine switch, we simple lock everything we need, then assign new state, then wake everybody who can be interested in changes. With channels it is simply impossible to do. It is true Cond does not mix well with other sync primitives. But if WaitTimeout is added to Cond, then we'd have no need to mix Cond with anything, because we'd then get rid of the rest channels in our code and remove tons of remaining inefficiency and wrappers around fact that we now need goroutine with timer which will wake up Cond only to implement Timeout. |
This proposal has been added to the active column of the proposals project |
Even in a v2 of sync, I find it very difficult to believe we would delete Cond. Condition variables are a fundamental building block for many abstractions. It's true that most users should use higher-level abstractions, but sync also exists to provide these building blocks, both Cond and Mutex. |
Worth noting that the sync.Cond doc comment is very good at pointing people at other things. |
Based on the discussion above, this proposal seems like a likely decline. |
For what it's worth I wrote this code over the weekend. Maybe there is a nice way to write it with a channel instead of a sync.Cond, but the sync.Cond seems to say exactly what I mean. I started out trying to use a channel but got frustrated by having to think about channel synchronization despite the fact that I was already holding a mutex.
|
No change in consensus, so declined. |
In the discussion on #16620, I've noticed that the majority of use-cases folks describe for
sync.Cond
turn out to work fine with a channel instead:(*sync.Cond).Broadcast
corresponds to callingclose
on the channel, and(*sync.Cond).Signal
corresponds to sending on the channel.The existence of
sync.Cond
suggests that people might want to use it, but it is currently under-documented (#20491), incompatible with other Go synchronization patterns (e.g.select
statements; see #16620), and unlike most other types in thesync
package, does not have a valid zero-value (itsWait
method requires a non-nilL
field). It has an additional "no copy" invariant enforced through both a run-time dynamic check and special-case code in thevet
tool.On top of that, condition variables are fiendishly difficult to use: they are prone to either missed or spurious signals [citation needed — experience reports welcome].
An audit of the Go standard library shows only a handful of uses:
io/pipe.go
: The use ofsync.Cond
was added in https://golang.org/cl/4252057. The previous implementation used channels and had no known bugs.syscall/net_nacl.go
: The comment there says "We do not use channels because we need to be able to handle writes after and during close, and because a chan byte would require too many send and receive operations in real use." It does not explain why these considerations preclude the use of channels (e.g. a separate 'chan struct{}' to signal closing, and channels of tokens or slices rather than channels of tokens or bytes).net/http/h2_bundle.go
: There are twosync.Cond
s in this file. One is is in a struct "likeio.Pipe
". The other is only used withBroadcast
(neverSignal
), and can thus be replaced with a channel that is closed to broadcast readiness.net/http/server.go
: Thissync.Cond
is again only used withBroadcast
, and thus easy to replace with a channel.crypto/tls/conn.go
: Again only used withBroadcast
.Of the above uses, only the one in
syscall/net_nacl.go
does not have an obvious channel equivalent. However, it appears to be used to limit the size of a buffer, and I know of at least one similar "limiter" API (x/sync/semaphore
) that is implemented in terms of channels in order to support interoperation with the standardcontext
package. (I did theCond
-to-channel conversion myself on a prototype of that package before it was open-sourced.)In light of the above observations, I propose that we remove the
Cond
type from thesync
package in Go 2.The text was updated successfully, but these errors were encountered: