Вопрос по concurrency, go, race-condition – Понимание горутин

39

Я пытаюсь понять параллелизм в 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).

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

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

Ваш Ответ

4   ответа
7

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

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

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

Вы можете запускать несколько одновременных потоков / процессов на одном процессоре, так что «только если ваш процессор имеет несколько ядер» вводит в заблуждение.
17

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

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

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() называетсяvery довольно часто. призваниеruntime.GOMAXPROCS(2) on init быстрее на порядок, но будет очень поточно-небезопасным, если вы делаете что-то более сложное, чем увеличение числа (например, работа с массивами, структурами, картами и т. д.).

В этом случае передовой практикой может стать использование канала для управления общим доступом к ресурсу.

Update: As of Go 1.2, любой не встроенный вызов функции может вызвать планировщик.

@ user793587 Зависит от того, что вы подразумеваете под «правильным». Опрос в тесном цикле может затянуть поток, но в любом случае код плохой. На практике это просто не проблема. В редком случае выneed Опрос в тесном цикле, вы можете явно уступить планировщику, но это обычно встречается в игрушечных примерах. Я слышал о планах перейти на упреждающий планировщик, но в большинстве случаев текущий кооперативный планировщик работает хорошо.
Правильно ли говорить, что эта проблема никогда не возникает при правильном использовании каналов? В противном случае это кажется большой проблемой с Go - если один поток может перегружать процессор, а другие потоки не могут его выполнить.
Если у вас несколько процессоров, вы также можете установить переменную окружения в GOMAXPROCS = 2, и тогда программа может выполняться в отдельном потоке, чем основная функция. Функция Gosched () сообщает среде выполнения, что нужно завершить цикл for.
35

They are not threads in the sense of Java's or C++ threads. They are more like greenlets. The go runtime multiplexes the goroutines across the system threads The number of system threads is controlled by an environment variable GOMAXPROCS and defaults to 1 currently I think. This may change in the future. The way goroutines yield back to their current thread is controlled by several different constructs. The select statement can yield control back to the thread. sending on a channel can yield control back to the thread. Doing IO operations can yield control back to the thread. runtime.Gosched() explicitly yields control back to the thread.

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

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

think тотinc_x загружает процессор. Поскольку здесь нет ввода-вывода, он не освобождает управление.

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

Похожие вопросы