[Перевод] Go: Горутины, потоки ОС и управление ЦП

Go

Оригинал https://medium.com/a-journey-with-go/go-goroutine-os-thread-and-cpu-management-2f5a5eaf518a

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

Расположение М, П, Г

Для решения этой проблемы в Go есть собственный планировщик, отвечающий за распределение горутин по потокам. Этот координатор состоит из 3 понятий, а именно:

The main concepts are:
G - goroutine.
M - worker thread, or machine. 工作线程或机器
P - processor, a resource that is required to execute Go code.
    M must have an associated P to execute Go code[...].
    处理者,负责执行Go代码, 每个M必须有一个关联的P去执行Go代码

Отношения между этими тремя таковы:

Каждая горунтина (G) работает в потоке операционной системы (M) и получает логический процессор (P). Давайте посмотрим, как Go справляется с ними на простом примере:

func main() {
   var wg sync.WaitGroup
   wg.Add(2)

   go func() {
      println(`hello`)
      wg.Done()
   }()

   go func() {
      println(`world`)
      wg.Done()
   }()

   wg.Wait()
}

Сначала Go создает разные P в зависимости от количества логических процессоров в машине и сохраняет их в виде списка свободных P.

Затем, когда новая горутина или горутина готова к запуску, она пробуждает бездействующий P, который создает M, связанный с потоком операционной системы.

Однако, когда P и M не работают, если нет горунтина, ожидающей выполнения, она вернет системный вызов системного вызова или даже будет принудительно остановлена ​​сборкой мусора и поместит ее обратно в свободный список P/M.

Когда программа запущена, Go создала несколько потоков ОС и связала файлы M. В нашем случае первый отвечает за печатьhellogoroutine будет использовать основную goroutine, а вторая goroutine получит P и M из свободного списка

Теперь, когда у нас есть общее представление о горутинах и управлении потоками, давайте посмотрим, когда в Go больше M, чем P, и как горутины управляют такими системными вызовами.

Системные вызовы

Go оптимизируется, оборачивая системные вызовы во время выполнения, блокируя или нет. Эта оболочка автоматически разорвет связь между P и M, а затем позволит второму потоку M запустить P. Давайте рассмотрим следующий пример чтения файла:

func main() {
   buf := make([]byte, 0, 2)

   fd, _ := os.Open("number.txt")
   fd.Read(buf)
   fd.Close()

   println(string(buf)) // 42
}

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

P0Теперь он доступен в свободном списке. После завершения системного вызова Go следует приведенным ниже правилам, пока не будет выполнено одно из условий.

  • Попробуйте получить идентичный P, в нашем случае этоP0, затем возобновить выполнение
  • Попытаться получить P в свободном списке, затем возобновить выполнение
  • поместите горутину в глобальную очередь, а затем поместите связанную с ней M обратно в список свободных

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

func main() {
   http.Get(`https://httpstat.us/200`)
}

Когда первый системный вызов завершается и явно указывает, что ресурс не готов, горутина приостанавливается до тех пор, пока сетевой опросчик не сообщит ей, что ресурс готов. В этом случае поток M не блокируется.

Когда координатор Go повторно находит незавершенную работу, горутина выполняется повторно один раз. После того, как координатор успешно получит сообщение, которого он ожидает, он спросит сетевой опросчик, есть ли какие-либо горутины, ожидающие запуска.

Если готово более одной горутины, оставшиеся горутины войдут в глобальную очередь исполняемых файлов и будут ждать выполнения.

Ограничение количества потоков ОС

Go не ограничивает количество потоков ОС, которые могут блокироваться при выполнении системного вызова, официальное объяснение:

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

Этот код объясняет ситуацию

func main() {
   var wg sync.WaitGroup

   for i := 0;i < 100 ;i++  {
      wg.Add(1)

      go func() {
         http.Get(`https://httpstat.us/200?sleep=10000`)

         wg.Done()
      }()
   }

   wg.Wait()
}

Ниже показано количество потоков, отображаемых в инструменте отслеживания.

Поскольку Go оптимизирует использование потоков, их можно использовать повторно, когда горутины заблокированы, что объясняет, почему это число не соответствует количеству циклов.