Оригинал 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. В нашем случае первый отвечает за печатьhello
goroutine будет использовать основную 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 оптимизирует использование потоков, их можно использовать повторно, когда горутины заблокированы, что объясняет, почему это число не соответствует количеству циклов.