Почему не запускается эта программа, даже с `time.Sleep`? - PullRequest
1 голос
/ 03 апреля 2020

Возьмите этот кусок кода:

func main() {
    var x int
    go func() {
        for {
            x++
        }
    }()
    time.Sleep(time.Second)
    fmt.Println("x =", x)
}

Почему x равно 0 в конце? Я понимаю, что планировщику Go нужен вызов time.Sleep() для вызова программы, но почему он этого не делает?

Подсказка: Установка time.Sleep() или вызов runtime.Gosched() внутри для l oop исправляет этот код. Но почему?

Обновление: Проверьте следующую версию того же кода:

func main() {
    var x int
    go func() {
        for i := 0; i < 10000; i++ {
            x++
        }
    }()
    time.Sleep(time.Second)
    fmt.Println("x =", x)
}

Любопытно, что код внутри goroutine теперь выполняется и x больше не равно 0. Оптимизирует ли компилятор здесь?

Ответы [ 2 ]

6 голосов
/ 03 апреля 2020

Важно знать, что вы спрашиваете здесь. В Go нет обещания, что эта программа будет делать что-то конкретное, потому что она недействительна. Но, как исследование оптимизатора, может быть интересно дать представление о том, как он реализован в настоящее время. Любая программа, которая опиралась на эту информацию, была бы очень fr agile и недействительной, но все же это любопытство.

Мы можем скомпилировать программу, а затем посмотреть на вывод. Мне особенно нравятся две версии, которые вы дали, потому что они позволяют видеть различия. Я выполнил декомпиляцию с использованием Hopper (они скомпилированы с помощью go1.14 darwin / amd64).

Во втором случае программа выглядит так, как вы думаете:

void _main.main.func1(int arg0, int arg1, int arg2, int arg3, int arg4, int arg5, int arg6) {
    rax = arg6;
    for (rcx = 0x0; rcx < 0x2710; rcx = rcx + 0x1) {
            *rax = *rax + 0x1;
    }
    return;
}

Ничего удивительного здесь нет. Но как насчет первого случая, который вас интересует:

_main.main.func1:
    goto _main.main.func1;

Он становится oop. Буквально; вот сборка:

                     _main.main.func1:
000000000109d1b0         nop                                                    ; CODE XREF=_main.main.func1+1
000000000109d1b1         jmp        _main.main.func1                            ; _main.main.func1

Как это происходит? Что ж, компилятор может посмотреть на этот код:

go func() {
    for {
        x++
    }
}()

И он знает, что ничто никогда не читает x. x невозможно что-либо прочитать, потому что вокруг x нет блокировки, и эта процедура никогда не завершается. Так что нет ничего, что могло бы прочитать x после , когда эта программа завершена. См. Go Модель памяти для получения дополнительной информации о том, что означает, что что-то должно произойти до чего-то другого или после чего-то другого.

"Но я действительно читаю х!" Нет, ты не Это будет неверный код, и компилятор знает, что вы не написали неверный код. Кто сделал бы это, когда есть детектор гонки, говорящий вам, что это недействительно? Так как компилятор может ясно видеть, что ничего не читается x, нет причин беспокоиться об его обновлении.

В вашем примере с ограниченным l oop выполнение программы завершается, поэтому вполне возможно, что что-то читает x после этого. Компилятор не достаточно умен, чтобы заметить, что правильное чтение никогда не выполняется, и поэтому он не оптимизирует это так хорошо, как мог бы. Возможно, будущий компилятор будет достаточно умен, чтобы вывести 0 в обоих случаях. И, возможно, будущий компилятор будет достаточно умен, чтобы полностью удалить вашу неоперативную программу в первом случае.

Но ключевой момент здесь заключается в том, что случай infinite-l oop полностью корректен, хотя и немного меньше эффективнее, чем могло бы быть. И случай с бесконечным числом l oop также совершенно корректен, хотя и гораздо менее эффективен, чем мог бы быть.

2 голосов
/ 03 апреля 2020

Это общая проблема многопроцессорности, не указанная ни для c ни для процедур, ни Go.

Нет гарантии на порядок выполнения операторов в вашем коде. Например, возможна следующая последовательность (при условии, что «G» - это ваша программа, а «M» - это код в main):

  1. M: x определено
  2. M : G определено и вызвано
  3. M: Sleep вызвано
  4. M: Sleep закончено
  5. M: Println (x = 0)
  6. G: x++
  7. G: x++
  8. ... (несколько раз, может быть даже 0)
  9. Конец программы

Для наблюдения за чередованием попробуйте:

package main

import (
    "fmt"
    "time"
)

func main() {
    var x int

    go func() {
        for {
            time.Sleep(time.Second) 
            x++
        }
    }()
    time.Sleep(5*time.Second)
    fmt.Println("x =", x)
}

Однако все еще нет никаких гарантий. Чтобы иметь какие-либо гарантии, используйте любую технику синхронизации, например каналы.

...