-
Notifications
You must be signed in to change notification settings - Fork 18k
time: Timer.Reset is not possible to use correctly #14038
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 disagree with the title. It is possible to use correctly. People often don't, but that's kind of a separate issue. The suggested Reset2 has its own race. If the Reset is concurrent with an active read on t.C, it is possible that stopTimer returns false but then the other goroutine's read on t.C takes the value before the <-t.C line in Reset2, causing Reset2 to hang. I believe this issue is primarily a duplicate of #11513. Perhaps we should do a better job of explaining how to use Reset correctly, but I don't think it would help to add a Reset2. |
Yes, I realized that Reset2 does not fix the second example, while it fixes the first one. Sorry, but your "corrected" version in #11513 (comment) has exactly the race I described and can hang forever (assuming that case <-time.After(t2) is actually an untrusted channel which can hang). |
FWIW, on a careful re-reading of Dmitriy's description of the races, it does seem like Reset is not possible to use correctly. The fundamental issue is that if Reset tells the caller that there is an event to read, there is now a race between the caller consuming that event in order to fully reset the channel and the new timer expiring, which will attempt to send a new event to the channel. For a long timeout this is likely never going to be an issue, but for short timeouts you could imagine it happening. Not thinking about what to do about this right now. It might be that we have to change Reset to reach in and drain the channel itself, as if implemented to start with Maybe for Go 1.8. I'd like to think about this again when I've had a good week's sleep. |
If I remember correctly, separate Stop and Start would solve the race. The crucial point is to be able to execute code (drain) in between Stop and Start. |
Hmm, so maybe Reset doesn't have to change, you just have to tell people you should always make sure the timer is stopped before calling Reset (so that Reset = Start) and never Reset a running timer. Anyway, for Go 1.8. |
CL https://golang.org/cl/23575 mentions this issue. |
Updates #14038 Fixes #14383 Change-Id: Icf6acb7c5d13ff1d3145084544c030a778482a38 Reviewed-on: https://go-review.googlesource.com/23575 Reviewed-by: Dmitry Vyukov <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Run-TryBot: Ian Lance Taylor <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
I think this change to documentation is not accurate. Here is a example with a slightly change from the first example of OP that adopt the doc, which does not concurrently receive on package main
import (
"time"
)
func main() {
timer := time.NewTimer(1 * time.Second)
for i := 0; i < 2; i++ {
if !timer.Stop() {
<-timer.C
}
timer.Reset(1 * time.Second)
<-timer.C
}
} I think the problem is that, the first call of both timeout := time.NewTimer(T)
for {
timeout.Reset(T)
select {
case <- ...: ...
if !timeout.Stop() {
<-timeout.C
}
case <-timeout.C: ...
}
} Or don't do that but just use a longer timeout duration and accept a concurrently timeout is still a timeout. The current behavior of package main
import (
"time"
)
func main() {
timer := time.NewTimer(1 * time.Second)
time.Sleep(2 * time.Second)
println(timer.Reset(1 * time.Second))
println(timer.Reset(1 * time.Second))
} will print true for the second There are still use cases for this behavior without first draining the old timer's channel, for example: // goroutine 1
for {
if err := doSomething(ctx); err != nil {
return err
}
timer.Reset(d)
}
// goroutine 2
<-timer.C
cancel() // call the CancelFunc of ctx |
It's not clear to me that the current doc is inaccurate. It says "to reuse an active timer." If you have already run |
Oh sorry, my bad. I was confused by the example in the doc. Edit: |
@rsc @ianlancetaylor What is left to do here? Are we going to change APIs or documentation, or is the existing change sufficient? |
Ping @dvyukov |
I don't think that adding more methods is useful here, it won't help with the old methods. |
CL https://golang.org/cl/31350 mentions this issue. |
Just implemented a small fix, which can be used as a drop-in replacement. |
I saw two common usage patterns of Timer.Reset:
First is ensuring that a sequence of events happens in a timely fashion:
Second is connection timeout with an ability to change the timeout:
There are several things that can go wrong here depending on timings:
I've found several cases of this bug in our internal codebase, one even with a TODO by @bradfitz describing this race.
Timer.Reset does the following:
What would be possible to use correctly is:
@rsc @Sajmani @aclements
The text was updated successfully, but these errors were encountered: