Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to stop a time.Timer that is currently being listened to in another go-routine?

Tags:

go

timer

I have an idle timeout timer being selected on in a goroutine, if I see activity I want to cancel the timer.

I had a look at the documentation and I'm not positive I'm clear on what it says.

func (t *Timer) Stop() bool
Stop prevents the Timer from firing. It returns true if the call stops the timer, false if the timer has already expired or been stopped. Stop does not close the channel, to prevent a read from the channel succeeding incorrectly.

To prevent a timer created with NewTimer from firing after a call to Stop, check the return value and drain the channel. For example, assuming the program has not received from t.C already:

if !t.Stop() { <-t.C }

This cannot be done concurrent to other receives from the Timer's channel.

I'm trying to understand when I have to drain the channel manually.

I'll list my understanding and if I'm wrong please correct me.

If Stop returned false this means either:

  • The timer was already stopped
    • In this case, reading from the channel will block so I shouldn't do it
  • The timer was already expired
    • Since I have another goroutine listening on the channel, can I know for certain if it has received the event?
    • From the "this cannot be done concurrent to other receives" part of the documentation it appears that this isn't an option, so what should I do?

In my case getting a superfluous event from the timer is no big deal, does that inform what I should do here?

like image 960
Motti Avatar asked Jun 28 '18 09:06

Motti


People also ask

How does Go timer work?

At each round of scheduling, it checks if the timers are ready to run, and if so, prepare them to run. Indeed, since the Go scheduler does not run any code itself, running the timer's callback will enqueue its goroutine to the local queue. Then, the goroutine will run when the scheduler picks it up in the queue.

Which statement timers and tickers waits for a specified duration?

Timeout (Timer) After waits for a specified duration and then sends the current time on the returned channel: select { case news := <-AFP: fmt. Println(news) case <-time.

What is the use of timers and tickers?

Timers — These are used for one-off tasks. It represents a single event in the future. You tell the timer how long you want to wait, and it provides a channel that will be notified at that time. Tickers — Tickers are exceptionally helpful when you need to perform an action repeatedly at given time intervals.

How do you run a timer in Golang?

The NewTimer() function in Go language is used to create a new Timer that will transmit the actual time on its channel at least after duration “d”. Moreover, this function is defined under the time package. Here, you need to import the “time” package in order to use these functions.


1 Answers

The reason that you might need to drain the channel is because of how goroutines are scheduled.

Problem

Imagine this case:

  1. Created a timer
  2. Timer fires, sending a value to t.C
  3. Nothing has received / read the value yet, but t.Stop() is called.

In this case there is a value on the channel t.C, and t.Stop() returns false because "the timer already expired" (i.e. when it sent the value on t.C).

The reason that the docs say "this cannot be done concurrent to other receives" is because there's not guarantee of ordering between the if !t.Stop { and the <-t.C. The stop command could return false, entering the if body. And then another goroutine could be scheduled and read the value from t.C that the body of the if statement was trying to drain. This would cause a datarace and result in blocking inside the if statement. (as you pointed out in your question!)

Solution

It depends on what the behaviour of the thing listening to the timer is.

If you are just in a simple select:

select {
    case result <- doWork():
    case <-t.C
}

Something like above. One of a few things could happen:

  1. The work could be done from doWork, sending the result, all good.
  2. The timer could fire, causing the timeout, breaking the select.
  3. Call to stop, stop the timer, only way out of the select is doWork() completing.
  4. The timer could fire, another routine calls t.Stop(), but it's too late because the value has been sent, causing the timeout, breaking the select.

As long as you are OK with case 4, you do not need to interact / drain the channel after calling Stop.

If you are not OK with case 4. You still cannot drain the t.C channel because there's another goroutine listening to it. This could block in the if statement. Instead you must find another way of laying out the code, or ensuring that your goroutine in the select is not still listening on the channel.

like image 68
Zak Avatar answered Nov 28 '22 20:11

Zak