time.Sleep приводит к нежелательному поведению

import "fmt"
import "time"

func main() {
    array := []int{1, 2, 3}
    for _, num := range array {
        go func() {
            fmt.Println(fucknum)
        }(fucknum)
        time.Sleep(time.Nanosecond)
    }
    time.Sleep(time.Second)
}

Поскольку внутри цикла for есть time.Sleep, я ожидал, что на выходе будет 1 2 3 из-за того, что выполнение выполняется на каждом time.Sleep.

Однако вывод этого фрагмента кода выводит 2 1 3. И после того, как я изменил Nanosecond на Microsecond, оказалось 1 2 3.

Для сравнения я также протестировал asyncio версию python3, в которой, как я полагаю, asyncio.call_soon эквивалентен не-IO сопрограмме Go.

import asyncio

loop = asyncio.get_event_loop()
async def test():
    for i in range(1, 4):
        # replace call_soon with asyncio.sleep(0) does not change the result
        loop.call_soon(lambda : print(i))
        await asyncio.sleep(0)

loop.run_until_complete(test())

И выходы всегда равны 1 2 3 (этот результат совпадает с тем, что я ожидал, поскольку я знаю, что внутри функции, запланированные call_soon, просто добавляются в очередь FIFO)

Как объяснить поведение версии Go?


person BAKE ZQ    schedule 02.10.2020    source источник
comment
Время выполнения горутин не гарантируется, как и порядок. Наносекунды или микросекунды может быть недостаточно, чтобы горутины запускали и запускали операторы печати.   -  person Marc    schedule 02.10.2020
comment
@Marc Значит, механизм планирования отличается от asyncio в Python?   -  person BAKE ZQ    schedule 02.10.2020
comment
Я понятия не имею, как работает asyncio, но горутине потребуется некоторое время для настройки и запуска после возврата оператора go. Когда это запланировано, тоже никак не гарантируется.   -  person Marc    schedule 02.10.2020
comment
@Marc Спасибо, не могли бы вы написать это в ответ, и я могу согласиться.   -  person BAKE ZQ    schedule 02.10.2020
comment
@Marc хорошо, я удалю этот вопрос. Еще раз спасибо за внимание.   -  person BAKE ZQ    schedule 02.10.2020


Ответы (1)


Поскольку каждая из сгенерированных горутин обращается к одной и той же переменной, определенной вне их области видимости, они не выделяют новый адрес памяти, а ссылаются на один и тот же адрес. Цикл for в вашем примере фактически ссылается на одну и ту же переменную несколько раз. Введя локальную область видимости в определение горутины, каждый раз, когда создается новая горутина, она будет выделять новую переменную на каждой итерации.

Чтобы решить проблему, вам нужно передать индекс в качестве параметра для функции закрытия.

Это должно решить вашу проблему.

func main() {
    array := []int{1, 2, 3}
    for _, num := range array {
        go func(num int) {
            fmt.Println(num)
        }(num)
        time.Sleep(time.Nanosecond)
    }
    time.Sleep(time.Second)
}
person Endre Simo    schedule 02.10.2020
comment
Этот. num изменяется перед печатью в другой горутине. Этого можно избежать, отправив параметр функции, которая выполняется в другой горутине. Но все же в этом примере нельзя гарантировать, что порядок будет 1 2 3, он также может быть 2 1 3 или 3 2 1 или что-то еще, в зависимости от порядка выполнения горутин. Если мы хотим обеспечить порядок, нам придется заблокировать. Хотя в этом случае мы могли бы не использовать параллелизм. - person Z. Kosanovic; 02.10.2020
comment
Почему порядок не может быть обеспечен? - person BAKE ZQ; 02.10.2020
comment
Поскольку горутины выполняются одновременно, они не ждут друг друга. - person Z. Kosanovic; 02.10.2020
comment
Потому что порядок выполнения горутин не гарантируется. Они будут созданы приостановленными, а затем, когда будет доступен временной интервал, планировщик запустит один из них, но политика планирования строго не указана, особенно с учетом кражи работы. - person Masklinn; 02.10.2020
comment
Суть параллелизма в том, что ваш код не заботится о том, в каком порядке он выполняется; с точки зрения вашего кода он выполняется одновременно. - person Adrian; 02.10.2020
comment
@Adrian Похоже, что это так, я раньше использовал python, и я знаю, что упорядочение сопрограмм следует путем FIFO, но это не похоже на то же самое в GO. - person BAKE ZQ; 02.10.2020
comment
@BAKEZQ это так, потому что продолжительность вашего сна составляет 5 миллисекунд в вашем коде Python, чего достаточно для выполнения сопрограммы. Увеличьте продолжительность сна в коде Go до 5 * time.Milliseconds, и он будет вести себя так же. - person Z. Kosanovic; 02.10.2020
comment
@Masklinn Можете сложить ответ, чтобы я мог принять ваш. - person BAKE ZQ; 02.10.2020
comment
@BAKEZQ они, естественно, не выполняют FIFO; если бы они это сделали, то в параллелизме буквально не было бы смысла. Они выполняются таким образом только потому, что вы искусственно заставляете их засыпать, гарантируя, что ваш параллельный код не будет выполняться одновременно. - person Adrian; 02.10.2020
comment
@ Адриан Я имею в виду функцию NON-IO. В случае версии IO обратные вызовы также выполняются в режиме FIFO. - person BAKE ZQ; 02.10.2020
comment
@ Z.Kosanovic проверьте мою обновленную версию Python. Не имеет значения, какова продолжительность. - person BAKE ZQ; 02.10.2020