Подтвердить что ты не робот

Понимание горти

Я пытаюсь понять concurrency в Go. В частности, я написал эту небезопасную программу:

package main

import "fmt"

var x = 1

func inc_x() { //test
  for {
    x += 1
  }
}

func main() {
  go inc_x()
  for {
    fmt.Println(x)
  }
}

Я понимаю, что я должен использовать каналы для предотвращения условий гонки с помощью x, но это не так. Программа печатает 1, а затем, как представляется, цикл навсегда (без печати ничего больше). Я ожидаю, что он напечатает бесконечный список чисел, возможно, пропустив некоторые и повторив другие из-за состояния гонки (или, что еще хуже, - напечатает номер, пока он обновляется в inc_x).

Мой вопрос: почему программа печатает только одну строку?

Просто для того, чтобы быть ясным: я не использую каналы для этого примера игрушек.

4b9b3361

Ответ 1

Есть несколько вещей, которые нужно помнить о Go Goroutines.

  1. Они не являются потоками в смысле потоков Java или C++
  2. Среда выполнения go мультиплексирует goroutines через системные потоки
    • Количество системных потоков контролируется переменной среды GOMAXPROCS и по умолчанию GOMAXPROCS 1, я думаю. Это может измениться в будущем
  3. Способ возврата goroutines к их текущему потоку контролируется несколькими различными конструкциями.
    • Оператор выбора может вернуть управление обратно в поток
    • отправка по каналу может вернуть управление обратно в поток
    • Выполнение операций ввода-вывода может вернуть управление потоку
    • runtime.Gosched() явно возвращает управление обратно в поток

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

Ответ 2

В соответствии с этим и этим некоторые вызовы не могут вызываться во время связанной с CPU Goroutine ( если Goroutine никогда не уступает планировщику). Это может привести к зависанию других Goroutines, если им нужно заблокировать основной поток (например, с помощью syscall write(), используемого fmt.Println())

Решение, которое я нашел, включало вызов runtime.Gosched() в потоке, связанный с cpu, чтобы вернуться к планировщику, следующим образом:

package main

import (
  "fmt"
  "runtime"
)

var x = 1

func inc_x() {
  for {
    x += 1
    runtime.Gosched()
  }
}

func main() {
  go inc_x()
  for {
    fmt.Println(x)
  }
}

Поскольку вы выполняете только одну операцию в Goroutine, runtime.Gosched() вызывается очень часто. Вызов runtime.GOMAXPROCS(2) на init быстрее на порядок, но будет очень небезопасным, если вы делаете что-то более сложное, чем приращение числа (например, имея дело с массивами, структурами, картами и т.д.).

В этом случае лучше всего использовать канал для управления совместным доступом к ресурсу.

Обновление: начиная с Go 1.2, любой вызов, не содержащий встроенных функций, может вызвать планировщик.

Ответ 3

Это взаимодействие двух вещей. Один, по умолчанию, Go использует только одно ядро ​​и два, Go должен планировать совместное использование goroutines. Ваша функция inc_x не уступает, и поэтому она монополизирует использование одного ядра. Освобождение любого из этих условий приведет к ожидаемому выходу.

Говоря "ядро", это немного блеск. Go может фактически использовать несколько ядер за кулисами, но использует переменную GOMAXPROCS для определения количества потоков для планирования ваших goroutines, которые выполняют несистемные задачи. Как объяснено в FAQ и Эффективный Go по умолчанию равен 1, но он может быть установлен выше с переменной окружения или функцией времени выполнения. Это, скорее всего, даст ожидаемый результат, но только если ваш процессор имеет несколько ядер.

Независимо от ядер и GOMAXPROCS, вы можете предоставить планировщику goroutine во время исполнения возможность выполнить эту работу. Планировщик не может вытеснить запущенный goroutine, но должен ждать, пока он вернется во время выполнения и запросит некоторую услугу, такую ​​как IO, time.Sleep() или runtime.Gosched(). Добавление чего-либо подобного в inc_x дает ожидаемый результат. Служба goroutine main() уже запрашивает службу с помощью fmt.Println, поэтому, когда два goroutines теперь периодически уступают во время выполнения, он может выполнять какое-то справедливое планирование.

Ответ 4

Не уверен, но я думаю, что inc_x забивает процессор. Поскольку там нет IO, он не освобождает управление.

Я нашел две вещи, которые решили это. Один из них состоял в том, чтобы называть runtime.GOMAXPROCS(2) в начале программы, а затем он будет работать, так как теперь есть два потока, обслуживающих goroutings. Другой - вставить time.Sleep(1) после увеличения x.