Подробно объясните таймер на языке Go

Go
Подробно объясните таймер на языке Go

GoВ стандартной библиотеке языка предусмотрено два типа таймеров.Timerа такжеTicker.TimerуказанныйdurationСрабатывает по прошествии времени, в собственное времяchannelОтправить текущее время, послеTimerОстановить время.Tickerлюбой другойdurationtime отправит текущую точку времени в свое собственное времяchannel, используя время таймераchannelМногие функции, связанные с таймингом, могут быть реализованы.

Статья в основном охватывает следующее содержание:

  • Timerа такжеTickerПредставление внутренней структуры таймера
  • Timerа такжеTickerКак использовать и меры предосторожности
  • как сделать это правильноResetтаймер

Внутреннее представление таймера

Оба таймера основаны наGoТаймеры времени выполнения для языковruntime.timerосуществленный,rumtime.timerСтруктура представлена ​​следующим образом:

type timer struct {
	pp puintptr

	when     int64
	period   int64
	f        func(interface{}, uintptr)
	arg      interface{}
	seq      uintptr
	nextwhen int64
	status   uint32
}

rumtime.timerЗначение полей в структуре

  • when— время пробуждения текущего таймера;
  • period— интервал между двумя пробуждениями;
  • f— функция, которая будет вызываться при каждом пробуждении таймера;
  • arg— вызывается при пробуждении таймераfвходящие параметры;
  • nextWhen- таймер включенtimerModifiedLater/timerModifiedEairlierстатус, используемый для установкиwhenполе;
  • status— состояние таймера;

здесьruntime.timerТолько собственное представление времени выполнения таймера, внешний таймерtime.Timerа такжеtime.TickerСтруктура представлена ​​следующим образом:

type Timer struct {
	C <-chan Time
	r runtimeTimer
}

type Ticker struct {
	C <-chan Time 
	r runtimeTimer
}

Timer.Cа такжеTicker.Cэто время в таймереchannel, давайте посмотрим, как использовать эти два таймера и на что следует обратить внимание при их использовании.

Таймер таймер

time.TimerТаймер должен пройтиtime.NewTimer,time.AfterFuncилиtime.Afterсоздание функции. Когда таймер истекает, истекшее время отправляется на таймер, удерживаемый таймером.channel,подпискаchannelизgoroutineполучит время, когда таймер истечет.

по таймеруTimerПользователи могут определить свою собственную логику тайм-аута, особенно при работе сselectобрабатывать несколькоchannelовертайм, синглchannelЭто особенно удобно в таких ситуациях, как тайм-аут чтения и записи.TimerОбычное использование выглядит следующим образом:

//使用time.AfterFunc:

t := time.AfterFunc(d, f)

//使用time.After:
select {
    case m := <-c:
       handle(m)
    case <-time.After(5 * time.Minute):
       fmt.Println("timed out")
}

// 使用time.NewTimer:
t := time.NewTimer(5 * time.Minute)
select {
    case m := <-c:
       handle(m)
    case <-t.C:
       fmt.Println("timed out")
}

time.AfterFuncсоздан таким образомTimer, по истечении тайм-аута он будет выполнен в отдельномgoroutineвыполнить функциюf.

func AfterFunc(d Duration, f func()) *Timer {
	t := &Timer{
		r: runtimeTimer{
			when: when(d),
			f:    goFunc,
			arg:  f,
		},
	}
	startTimer(&t.r)
	return t
}

func goFunc(arg interface{}, seq uintptr) {
	go arg.(func())()
}

сверхуAfterFuncИсходный код виден снаружиfПараметр напрямую не назначается таймеру времени выполнения.f, но как функция-оболочкаgoFuncпереданные параметры.goFuncначнется новыйgoroutineдля выполнения внешних переданных функцийf. Это связано с тем, что все функции событий таймера создаютсяGoуникальный во время выполненияgoroutine timerprocВ ходе выполнения. чтобы не блокироватьtimerprocвыполнение, должен начать новыйgoroutineВыполнить функцию события с истекшим сроком действия.

заNewTimerа такжеAfterДва метода созданияTimerПо истечении таймаута выполнить функцию, встроенную в стандартную библиотеку:sendTime.

func NewTimer(d Duration) *Timer {
	c := make(chan Time, 1)
	t := &Timer{
		C: c,
		r: runtimeTimer{
			when: when(d),
			f:    sendTime,
			arg:  c,
		},
	}
	startTimer(&t.r)
	return t
}

func sendTime(c interface{}, seq uintptr) {
	select {
	case c.(chan Time) <- Now():
	default:
	}
}

sendTimeОтправить текущее время наTimerвремяchannelсередина. то это действие не заблокируетсяtimerprocвыполнение? Ответ нет, причина в томNewTimerсоздает буферизованныйchannelТак что неважноTimer.CэтоchannelЕсть ли получательsendTimeможет отправлять текущее время без блокировки наTimer.C,а такжеsendTimeТакже добавлена ​​двойная страховка: черезselectсудитьTimer.CизBufferНезависимо от того, заполнен ли он, как только он будет заполнен, он выйдет напрямую без блокировки.

TimerизStopметод предотвращения срабатывания таймера, вызовитеStopМетод успешно останавливает срабатывание таймера и возвращаетсяtrue, если таймер истек или былStopостановлено, звоните сноваStopметод вернетfalse.

GoСреда выполнения поддерживает все таймеры в минимальной кучеMin Heapсередина,StopТаймер просто удаляет этот таймер из кучи.

Тикерный таймер

TickerВременные события могут запускаться периодически, каждый раз, когда достигается указанный временной интервал, событие будет запускаться.

time.Tickerнужно пройтиtime.NewTickerилиtime.TickСоздайте.

// 使用time.Tick:
go func() {
	for t := range time.Tick(time.Minute) {
		fmt.Println("Tick at", t)
	}
}()

// 使用time.Ticker
var ticker *time.Ticker = time.NewTicker(1 * time.Second)

go func() {
    for t := range ticker.C {
        fmt.Println("Tick at", t)
    }
}()

time.Sleep(time.Second * 5)
ticker.Stop()     
fmt.Println("Ticker stopped")

ноtime.TickРедко используется, если только вы не хотите использовать его на протяжении всего времени жизни программы.time.Tickerвремяchannel. в официальном документеtime.TickОписание:

time.TickбазаTickerне может быть восстановлен сборщиком мусора;

так что используйтеtime.TickБудьте осторожны при использовании, чтобы избежать случайного использованияtime.NewTickerвернутьTickerальтернатива.

NewTickerСоздайте таймер сNewTimerВремя, удерживаемое созданным таймеромchannelс тайникомchannel, функция, выполняемая после каждого триггера, такжеsendTime, что гарантирует, что независимо от ошибочного приемникаTickerНи один из них не будет блокироваться при запуске события времени:

func NewTicker(d Duration) *Ticker {
	if d <= 0 {
		panic(errors.New("non-positive interval for NewTicker"))
	}
	// Give the channel a 1-element time buffer.
	// If the client falls behind while reading, we drop ticks
	// on the floor until the client catches up.
	c := make(chan Time, 1)
	t := &Ticker{
		C: c,
		r: runtimeTimer{
			when:   when(d),
			period: int64(d),
			f:      sendTime,
			arg:    c,
		},
	}
	startTimer(&t.r)
	return t
}

Проблемы, о которых следует помнить при сбросе таймера

оResetПредложение по использованию, описание в документе:

Необходимо соблюдать осторожность при сбросе таймера, чтобы не конкурировать с истечением текущего таймера, отправляя время в t.C. Если программа уже получила значение от t.C, известно, что таймер истек, и можно напрямую использовать t.Reset. Если программа не получила значения от t.C, то таймер должен быть сначала остановлен, и - если при использовании t.Stop сообщается, что таймер истек, то слить значение его канала.

Например:

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

В примере нижеproducer goroutineотправлять один на канал каждую секундуfalseзначение, подождите одну секунду после завершения цикла, а затем отправьте его в каналtrueценность. существуетconsumer goroutineЗдесь он пытается прочитать значение из канала через цикл и устанавливает максимальное время ожидания на 5 секунд с помощью таймера.Если время ожидания таймера истекло, выведите текущее время и попробуйте следующий цикл, если чтение из канала не является ожидаемым значением (ожидаемое значение равноtrue), попробуйте снова прочитать с канала и сбросьте таймер.

func main() {
    c := make(chan bool)

    go func() {
        for i := 0; i < 5; i++ {
            time.Sleep(time.Second * 1)
            c <- false
        }

        time.Sleep(time.Second * 1)
        c <- true
    }()

    go func() {
        // try to read from channel, block at most 5s.
        // if timeout, print time event and go on loop.
        // if read a message which is not the type we want(we want true, not false),
        // retry to read.
        timer := time.NewTimer(time.Second * 5)
        for {
            // timer is active , not fired, stop always returns true, no problems occurs.
            if !timer.Stop() {
                <-timer.C
            }
            timer.Reset(time.Second * 5)
            select {
            case b := <-c:
                if b == false {
                    fmt.Println(time.Now(), ":recv false. continue")
                    continue
                }
                //we want true, not false
                fmt.Println(time.Now(), ":recv true. return")
                return
            case <-timer.C:
                fmt.Println(time.Now(), ":timer expired")
                continue
            }
        }
    }()

    //to avoid that all goroutine blocks.
    var s string
    fmt.Scanln(&s)
}

Вывод программы следующий:

2020-05-13 12:49:48.90292 +0800 CST m=+1.004554120 :recv false. continue
2020-05-13 12:49:49.906087 +0800 CST m=+2.007748042 :recv false. continue
2020-05-13 12:49:50.910208 +0800 CST m=+3.011892138 :recv false. continue
2020-05-13 12:49:51.914291 +0800 CST m=+4.015997373 :recv false. continue
2020-05-13 12:49:52.916762 +0800 CST m=+5.018489240 :recv false. continue
2020-05-13 12:49:53.920384 +0800 CST m=+6.022129708 :recv true. return

Пока проблем нет.Использование Reset для сброса таймера тоже работает.Далее надоproducer goroutinвнести некоторые изменения, мы ставимproducer goroutineИзмените логику отправки значений каждую секунду на каждый6секунд для отправки значения, в то время какconsumer gouroutineпробег и таймер еще5Истекает в секундах.

  // producer
	go func() {
		for i := 0; i < 5; i++ {
			time.Sleep(time.Second * 6)
			c <- false
		}

		time.Sleep(time.Second * 6)
		c <- true
	}()

Запустите его снова, и вы обнаружите, что программа произошлаdeadlockЗаблокировано сразу после истечения таймера первого отчета:

2020-05-13 13:09:11.166976 +0800 CST m=+5.005266022 :timer expired


Где заблокирована программа? Да, это истощаетtimer.CПри перекрытии канала (по-английски дренажный канал уподобляется сливу воды в трубопроводе, в программеtimer.CВ конвейере больше нет неполученных значений).

if !timer.Stop() {
    <-timer.C
}
timer.Reset(time.Second * 5)

producer goroutineповедение отправки изменилось,comsumer goroutineЕсть событие, что таймер истекает до получения первых данных,forПерейдите к следующему циклу. В настоящее времяtimer.Stopфункция больше не возвращаетtrue, ноfalse, поскольку срок действия таймера истек, упомянутая выше минимальная куча, в которой хранятся все активные таймеры, больше не содержит таймер. В настоящее времяtimer.CНет данных вdrain channelКод будетconsumer goroutineзаблокирован.

В этом случае следует непосредственноResetтаймер вместо явногоdrain channel. Как совместить эти две ситуации в одну? мы можем использоватьselectобернутьdrain channelоперации, так что независимо отchannelесть ли данные вdrainне заблокирует.

//consumer
    go func() {
        // try to read from channel, block at most 5s.
        // if timeout, print time event and go on loop.
        // if read a message which is not the type we want(we want true, not false),
        // retry to read.
        timer := time.NewTimer(time.Second * 5)
        for {
            // timer may be not active, and fired
            if !timer.Stop() {
                select {
                case <-timer.C: //try to drain from the channel
                default:
                }
            }
            timer.Reset(time.Second * 5)
            select {
            case b := <-c:
                if b == false {
                    fmt.Println(time.Now(), ":recv false. continue")
                    continue
                }
                //we want true, not false
                fmt.Println(time.Now(), ":recv true. return")
                return
            case <-timer.C:
                fmt.Println(time.Now(), ":timer expired")
                continue
            }
        }
    }()

Запустите модифицированную программу и обнаружите, что программа не будет заблокирована, а чтение каналов может выполняться в обычном режиме.trueПосле значения он выйдет сам по себе. Результат выглядит следующим образом:

2020-05-13 13:25:08.412679 +0800 CST m=+5.005475546 :timer expired
2020-05-13 13:25:09.409249 +0800 CST m=+6.002037341 :recv false. continue
2020-05-13 13:25:14.412282 +0800 CST m=+11.005029547 :timer expired
2020-05-13 13:25:15.414482 +0800 CST m=+12.007221569 :recv false. continue
2020-05-13 13:25:20.416826 +0800 CST m=+17.009524859 :timer expired
2020-05-13 13:25:21.418555 +0800 CST m=+18.011245687 :recv false. continue
2020-05-13 13:25:26.42388 +0800 CST m=+23.016530193 :timer expired
2020-05-13 13:25:27.42294 +0800 CST m=+24.015582511 :recv false. continue
2020-05-13 13:25:32.425666 +0800 CST m=+29.018267054 :timer expired
2020-05-13 13:25:33.428189 +0800 CST m=+30.020782483 :recv false. continue
2020-05-13 13:25:38.432428 +0800 CST m=+35.024980796 :timer expired
2020-05-13 13:25:39.428343 +0800 CST m=+36.020887629 :recv true. return

Суммировать

Выше описано более подробноGoЯзыковые таймеры, их использование и меры предосторожности кратко изложены в следующих ключевых моментах:

  • Timerа такжеTickerтаймер времени выполненияruntime.timerреализовано на основе .
  • Функции событий всех таймеров в среде выполнения контролируются уникальной средой выполнения.goroutine timerprocвызывать.
  • time.TickсозданныйTickerне будетgcПереработка, если вы не можете его использовать.
  • Timerа такжеTickerвремяchannelОба канала с буфером.
  • time.After,time.NewTimer,time.NewTickerСозданный таймер будет выполнен, когда он сработаетsendTime.
  • sendTimeА буферизованный временной канал таймера гарантирует, что таймер не блокирует программу.
  • ResetСледите за таймерамиdrain channelЕсть состояние гонки с истечением таймера.