Как устроена функция time.Sleep() в Golang
Содержание
Вам наверняка не раз доводилось писать такой код:
func main() {
// ...
time.Sleep(4 * time.Second)
// ...
}
Он заставляет программу (горутину) подождать указанное количество секунд. Но что происходит внутри функции time.Sleep()
? Давайте разберемся.
Как работает time.Sleep()
Давайте сразу посмотрим реализацию (go1.19):
// timeSleep puts the current goroutine to sleep for at least ns nanoseconds.
//
//go:linkname timeSleep time.Sleep
func timeSleep(ns int64) {
if ns <= 0 {
return
}
gp := getg()
t := gp.timer
if t == nil {
t = new(timer)
gp.timer = t
}
t.f = goroutineReady
t.arg = gp
t.nextwhen = nanotime() + ns
if t.nextwhen < 0 { // check for overflow.
t.nextwhen = maxWhen
}
gopark(resetForSleep, unsafe.Pointer(t), waitReasonSleep, traceEvGoSleep, 1)
}
Разберёмся, что здесь происходит:
- Первым делом мы проверяем, что
ns
больше нуля. Если меньше, то просто возвращаемся.ns
- это количество наносекунд, которые мы хотим подождать. - Далее мы получаем текущую горутину с помощью
getg()
. Эта функция возвращает указатель на структуруg
, которая описывает горутину - Получаем таймер из горутины с помощью
gp.timer
. Если таймера нет, то мы создаем новый. Это нужно для того, чтобы не создавать новый таймер на каждый вызовtime.Sleep()
, переиспользуя существующий - это экономит память и время
Таймер - это структура, описывающая событие, которое должно произойти в будущем. В нашем случае такое событие - это готовность горутины - Устанавливаем функцию, которая будет вызвана по истечении таймера и аргументы для этой функции.
ФункцияgoroutineReady
просто устанавливает флагready
вtrue
для указанной горутины.gp
- это указатель на текущую горутину, который мы получили выше - Устанавливаем время, когда таймер должен сработать: получаем текущее время в наносекундах с помощью
nanotime()
, прибавляем к немуns
и сохраняем вt.nextwhen
- Если
t.nextwhen < 0
, значит произошло переполнение. В этом случае мы устанавливаемt.nextwhen
равнымmaxWhen
(максимальное значениеint64
) - Вызываем
gopark()
для ожидания.gopark()
- это функция, которая переводит горутину в состояние ожидания. В нашем случае до тех пор, пока не сработает таймер. У неё пять аргументов:- Функция, которая будет вызвана, когда таймер сработает. В нашем случае это
resetForSleep()
, которая сбрасывает таймер - Указатель на таймер, который мы создали или получили выше
- Причина, по которой горутина переводится в состояние ожидания. В нашем случае это
waitReasonSleep
- Событие, которое будет записано в трассировку. В нашем случае это
traceEvGoSleep
- Флаг, который указывает, что горутина должна быть заблокирована
- Функция, которая будет вызвана, когда таймер сработает. В нашем случае это
- Когда таймер сработает, мы вызываем
goroutineReady()
, которая устанавливает флаг горутиныready
вtrue
.
Теперь, когда состояние горутины ready
, ей осталось лишь дождаться, когда планировщик её снова запустит.