Содержание

Как устроена функция 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() - это функция, которая переводит горутину в состояние ожидания. В нашем случае до тех пор, пока не сработает таймер. У неё пять аргументов:
    1. Функция, которая будет вызвана, когда таймер сработает. В нашем случае это resetForSleep(), которая сбрасывает таймер
    2. Указатель на таймер, который мы создали или получили выше
    3. Причина, по которой горутина переводится в состояние ожидания. В нашем случае это waitReasonSleep
    4. Событие, которое будет записано в трассировку. В нашем случае это traceEvGoSleep
    5. Флаг, который указывает, что горутина должна быть заблокирована
  • Когда таймер сработает, мы вызываем goroutineReady(), которая устанавливает флаг горутины ready в true.

Теперь, когда состояние горутины ready, ей осталось лишь дождаться, когда планировщик её снова запустит.