Главной особенностью Golang является Goroutine. Goroutine — важная гарантия того, что Golang поддерживает параллелизм. Golang может создавать тысячи горутин для обработки задач.Модель GMP используется для выделения, загрузки и планирования этих горутин для процессоров.
что такое горутина
Горутин = Голанг + Корутин.Goroutine — это сопрограмма, реализованная golang и являющаяся потоком пользовательского уровня.. Горутины имеют следующие характеристики:
- По сравнению с потоками его стоимость запуска очень мала, и он начинается с небольшого пространства стека (около 2 КБ).
- Размер стека может динамически масштабироваться, а максимальный может поддерживаться до уровня Гб.
- Работайте в пользовательском режиме, переключайтесь на очень маленькие
- Связь с потоками — n:m, то есть m горутин можно запланировать на n системных потоков.
Процесс, Поток, Горутина
В операционной системе, которая поддерживает только процессы, процесс является базовой единицей, которая владеет ресурсами и планируется независимо. В операционных системах, которые представили потоки,Поток — это основная единица независимого планирования, а процесс — это основная единица владения ресурсами.. В том же процессе переключение потоков не вызывает переключения процесса. Переключение потоков в разных процессах, например переключение с потока в одном процессе на поток в другом процессе, вызовет переключение процесса.
Метод создания потока, управления, планирования и т. д. называется моделью потока. Резьбовые модели обычно делятся на следующие три типа:
- Модель потока на уровне ядра
- Модель потока на уровне пользователя
- Двухуровневая модель потоков, также известная как гибридная модель потоков.
Самая большая разница между тремя моделями потоков заключается в соответствии между потоками пользовательского уровня и объектом планирования ядра (KSE).KSE — это сокращение от Kernel Scheduling Entity. Это объектный объект, который может планироваться планировщиком ядра операционной системы. Это наименьшая единица планирования ядра операционной системы, которую можно просто понимать как поток уровня ядра..
Потоки пользовательского уровня — это сопрограммы., созданная и управляемая приложением, сопрограмма должна быть привязана к потоку уровня ядра, прежде чем ее можно будет выполнить.Планирование потоков ЦП является упреждающим, а планирование сопрограмм в пользовательском режиме — совместным.После того, как сопрограмма отказывается от ЦП, выполняется следующая сопрограмма..
Потоки уровня пользователя (ULT) по сравнению с потоками уровня ядра (KLT):
характеристика | Поток уровня пользователя | потоки уровня ядра |
---|---|---|
создатель | применение | ядро |
Знает ли операционная система о существовании | нет | да |
накладные расходы | Низкая стоимость создания и низкая стоимость переключения контекста, переключение контекста не требует аппаратной поддержки | Высокая стоимость создания, высокая стоимость переключения контекста, переключение контекста требует аппаратной поддержки |
Если поток блокируется | Весь процесс будет заблокирован. То есть многопроцессорность не может быть использована для получения преимуществ параллелизма. | Другие потоки могут продолжать выполняться, процесс не будет заблокирован |
кейс | Java thread, POSIX threads | Window Solaris |
Модель потоков на уровне ядра
В модели многопоточности на уровне ядра существует отношение один к одному между пользовательскими потоками и потоками ядра (1:1)..Создание потока, уничтожение, переключение работы ядра выполнено. Приложение не участвует в управлении потоками и может вызывать только интерфейс программирования потоков на уровне ядра (когда приложение создает новый поток или отменяет существующий поток, будет выполнен системный вызов). Каждый пользовательский поток связан с потоком ядра. Пользовательские потоки привязаны к этому потоку ядра в течение всего времени своего существования. Как только пользовательский поток завершится, оба потока покинут систему.
Планировщик операционной системы управляет, планирует и распределяет эти потоки. Библиотека времени выполнения запрашивает один поток уровня ядра для каждого потока пользовательского уровня. Подсистемы управления памятью и планирования операционной системы должны учитывать большое количество потоков пользовательского уровня. Операционная система создает контекст для каждого потока. Каждый поток процесса может быть назначен ядру процессора, когда ресурсы доступны.
Модель потоков на уровне ядра имеет следующие преимущества:
- В многопроцессорной системе ядро может выполнять несколько потоков в рамках одного и того же процесса параллельно.
- Если один поток в процессе заблокирован, другие потоки не будут заблокированы, и другие потоки в том же процессе могут быть переключены для продолжения выполнения.
- Когда поток заблокирован, ядро может запустить поток другого процесса в соответствии с выбором, а в потоке, реализованном в пользовательском пространстве, система выполнения всегда запускает поток в своем собственном процессе.
недостаток:
- Создание и удаление потоков требует участия ЦП, что является дорогостоящим.
Модель потоков на уровне пользователя
Пользовательские потоки в модели пользовательских потоков имеют отношение «многие к одному» с потоком ядра KSE (N : 1)..Создание и уничтожение потоков, а также координация и синхронизация между потоками выполняются в пользовательском режиме., в частности, выполняется библиотекой потоков приложения.Ядро не знает об этом, и планирование ядра в это время основано на процессе. С макроскопической точки зрения каждый процесс может одновременно иметь только один поток, и процессу будет выделено только одно ядро процессора.
Как видно из рисунка выше: планировщик библиотеки выбирает поток из множества потоков процесса, а затем этот поток связывается с потоком ядра, разрешенным процессом. Потоки ядра будут назначены ядрам процессора планировщиком операционной системы. Потоки пользовательского уровня — это сопоставление потоков «многие к одному».
Потоки пользовательского уровня имеют следующие преимущества:
- Затраты на управление потоками, такие как создание и уничтожение потоков, затраты на переключение потоков и т. д., намного меньше, чем у потоков ядра, поскольку процесс сохранения состояния потока и вызывающая программа являются только локальными процессами.
- Потоки могут использовать больше табличного пространства и пространства стека, чем потоки уровня ядра.
недостаток:
- Когда поток блокируется из-за ошибки ввода-вывода или страницы, если вызывается блокирующий системный вызов, ядро блокирует весь процесс и блокирует все потоки, потому что оно не знает, что существует несколько потоков. находиться в одном и том же процессе в одно и то же время.
- Планирование ресурсов выполняется в соответствии с процессом.При наличии нескольких процессоров потоки одного и того же процесса могут быть разделены по времени только под управлением одного и того же процессора.
Двухуровневая модель потоков
В двухуровневой модели потоков существует отношение один к одному между пользовательскими потоками и потоками ядра (N : M).. Модель двухуровневой нити полностью поглощает преимущества двух вышеперечисленных моделей и максимально избегает недостатков. Создание потока выполняется в пользовательском пространстве, а планирование потока и синхронизация также выполняются в приложении. Несколько потоков пользовательского уровня в приложении связаны с некоторыми (меньше или равными количеству потоков пользовательского уровня) потоками уровня ядра.
Поточная модель Голанга
Golang реализует гибридную модель многопоточности внизу.. M — это системный поток, который генерируется системным вызовом.M связан с KSE, то есть с системным потоком в двухуровневой модели потока. G — это Grooutine, то есть приложение и поток двухуровневой модели потока. Отношение между M и G равно N:M.
Обзор модели GMP
G-M-P означает:
- G — горутина, сопрограмма Go, это наименьшая единица, участвующая в планировании и выполнении.
- M - Машина, относящаяся к потокам системного уровня.
- P - Процессор, относится к логическому процессору, локальная рабочая очередь G (также называемая LRQ), связанная с P, которая может хранить до 256 G.
Процесс планирования GMP примерно выглядит следующим образом:
- Если поток М хочет выполнить задачу, ему необходимо получить Р, связанный с Р.
- затем получить G из локальной очереди P (LRQ)
- Если в LRQ нет исполняемого G, M попытается получить партию G из глобальной очереди (GRQ) и поместить ее в локальную очередь P,
- Если глобальная очередь не находит исполняемый G, M случайным образом украдет половину этого из локальных очередей других P и поместит его в локальную очередь своего P.
- Получив исполняемый G, M запускает G, а после выполнения G M получает следующий G от P и повторяет.
Запланированный жизненный цикл
- M0 — это основной поток с номером 0 после запуска программы. Экземпляр, соответствующий этому M, будет находиться в глобальной переменной runtime.m0 и не нуждается в размещении в куче. M0 отвечает за выполнение операции инициализации и запуск первого G. После этого M0 будет таким же, как и другие M
- G0 — это первая гуртина, создаваемая каждый раз при запуске M. G0 используется только для планирования G, G0 не указывает на какую-либо исполняемую функцию, и каждый M будет иметь свой собственный G0. Пространство стека G0 будет использоваться при планировании или системных вызовах, а G0 глобальных переменных — это G0 M0.
Приведенное выше описание процесса жизненного цикла:
- Среда выполнения создает начальный поток m0 и горутину g0 и связывает их (g0.m = m0).
- Инициализация планировщика: установите максимальное количество M, количество P, сбоев стека и памяти и создайте GOMAXPROCS P
- Основная функция в примере кода — main.main, а в рантайме есть основная функция — runtime.main, после компиляции кода runtime.main вызовет main.main, а при запуске программы горутина быть создан для runtime.main, который называется Let it be the main goroutine, а затем добавить основную goroutine в локальную очередь P.
- Запустите m0, m0 связал P, получит G из локальной очереди P и получит основную горутину.
- G владеет стеком, а M устанавливает рабочую среду в соответствии с информацией о стеке и информацией о планировании в G.
- М беги Г
- G завершает работу, снова возвращается к M, чтобы получить готовую к выполнению G, и так далее, пока main.main не выйдет, runtime.main не выполнит обработку Defer и Panic или не вызовет runtime.exit для выхода из программы.
Количество Г-М-П
Количество G:
Теоретически верхнего предела числа нет. Просмотрите текущее количество G, которое можно использоватьruntime. NumGoroutine()
Количество ПС:
Переменные среды запуска$GOMAXPROCS
или поruntime.GOMAXPROCS()
Принять решение. Это означает, что во время выполнения программы одновременно выполняются только горутины $GOMAXPROCS.
Количество М:
Ограничение самого языка go: при запуске программы go будет установлено максимальное количество М, которое по умолчанию равно 10000. Однако ядру сложно поддерживать такое большое количество потоков, поэтому данное ограничение может быть проигнорировано. Функция SetMaxThreads во время выполнения/отладки, чтобы установить максимальное количество M М блокируется и создается новый М. Нет абсолютной связи между M и количеством P. Когда один M заблокирован, P создаст или переключит другой M. Следовательно, даже если количество P по умолчанию равно 1, может быть создано много M.
Статус запланированного процесса
Из приведенного выше рисунка мы видим, что:
- У каждого P есть локальная очередь, и локальная очередь содержит горутины для выполнения (процесс 2).Когда локальная очередь P, связанная с M, заполнена, горутина будет помещена в глобальную очередь (процесс 2-1).
- Каждый P связан с M, M — это объект, который фактически выполняет горутину в P (процесс 3), и M получает G из локальной очереди в связанном P для выполнения.
- Когда локальная очередь P, связанная с M, пуста, M получит локальную очередь из глобальной очереди для выполнения G (процесс 3.1).Похищение G из локальной очереди для выполнения (процесс 3.2), этот способ кражи у других P называетсяwork stealing
- Когда G заблокирован системным вызовом (syscall), будет заблокирован M. В это время P будет отвязан от M.hand off, и искать новый неработающий М, если нет неработающего М, будет создан новый М (процесс 5.1).
- Когда G заблокирован из-за канального или сетевого ввода-вывода, M не будет заблокирован, а M будет искать G других исполняемых модулей; когда заблокированный G восстановится, он снова войдет в исполняемый модуль и войдет в очередь P для ожидания выполнение (процесс 5.3)
Блокировка во время планирования
Блокировка модели GMP может произойти в следующих ситуациях:
- ввод/вывод, выберите
- block on syscall
- channel
- ожидание блокировки
- runtime.Gosched()
Блокировка пользовательского режима
Когда горутина блокируется из-за операции канала или сетевого ввода-вывода (фактически, golang использовал netpoller, чтобы понять, что блокировка сетевого ввода-вывода горутиной не приведет к блокировке M, а только заблокирует G), соответствующий G будет помещен в очереди ожидания (например, waitq канала) состояние G изменяется с _Gruning на _Gwaitting, и M пропустит G, чтобы попытаться получить и выполнить следующий G. на этот раз M разрешает Bind P и переходит в состояние сна; когда заблокированный G пробуждается G2 на другом конце (например, уведомление о чтении/записи канала), G помечается как работоспособный, попробуйте присоединиться следующий за P, где находится G2, а затем Local of P Queue и Global Queue.
блокировка системных вызовов
Когда G заблокирован в системном вызове, G будет заблокирован в состоянии _Gsyscall, а M также находится в состоянии блокировки в состоянии системного вызова. В это время M можно вытеснить и запланировать: M, выполняющий G, будет освобожден от П, А П пытается связать с М другого бездействующего и продолжает выполнять другой Г. Если нет другого незанятого M, но в локальной очереди P все еще есть G для выполнения, создается новый M; когда системный вызов завершен, G повторит попытку получить незанятый P и войдет в свою локальную очередь для выполнения. возобновить выполнение. Без простоя P, G будет помечен как работоспособный и добавлен в глобальную очередь.
Внутренняя структура GMP
Внутренняя структура G
Важными полями внутренней структуры G являются следующие.Полную структуру см.исходный код
type g struct {
stack stack // g自己的栈
m *m // 隶属于哪个M
sched gobuf // 保存了g的现场,goroutine切换时通过它来恢复
atomicstatus uint32 // G的运行状态
goid int64
schedlink guintptr // 下一个g, g链表
preempt bool //抢占标记
lockedm muintptr // 锁定的M,g中断恢复指定M执行
gopc uintptr // 创建该goroutine的指令地址
startpc uintptr // goroutine 函数的指令地址
}
Состояние G имеет следующие 9 видов, вы можете обратиться ккод:
государство | ценность | значение |
---|---|---|
_Gidle | 0 | Просто выделено, еще не инициализировано. |
_Grunnable | 1 | Уже находящийся в очереди выполнения пользовательский код еще не выполнен. |
_Grunning | 2 | Нет в очереди на выполнение, пользовательский код уже может быть выполнен, M и P на данный момент назначены. |
_Gsyscall | 3 | Выполняется системный вызов, и M выделен. |
_Gwaiting | 4 | Заблокирован во время выполнения, пользовательский код не выполняется, и он не находится в очереди на выполнение, в этот момент он заблокирован где-то в ожидании. Каковы причины ожидания Гроутина?код |
_Gmoribund_unused | 5 | Еще не используется, но жестко запрограммировано в gdb. |
_Gdead | 6 | Пока не используется, это состояние может быть только что завершено или только инициализировано.В настоящее время оно не выполняет пользовательский код и может выделять или не выделять стек. |
_Genqueue_unused | 7 | Неиспользованный. |
_Gcopystack | 8 | Стек копируется, пользовательский код не выполняется и его нет в очереди выполнения. |
Структура М
Внутреннее строение М, полную структуру см.исходный код
type m struct {
g0 *g // g0, 每个M都有自己独有的g0
curg *g // 当前正在运行的g
p puintptr // 隶属于哪个P
nextp puintptr // 当m被唤醒时,首先拥有这个p
id int64
spinning bool // 是否处于自旋
park note
alllink *m // on allm
schedlink muintptr // 下一个m, m链表
mcache *mcache // 内存分配
lockedg guintptr // 和 G 的lockedm对应
freelink *m // on sched.freem
}
Внутренняя структура P
Внутреннее строение P, полную структуру см.исходный код
type p struct {
id int32
status uint32 // P的状态
link puintptr // 下一个P, P链表
m muintptr // 拥有这个P的M
mcache *mcache
// P本地runnable状态的G队列,无锁访问
runqhead uint32
runqtail uint32
runq [256]guintptr
runnext guintptr // 一个比runq优先级更高的runnable G
// 状态为dead的G链表,在获取G时会从这里面获取
gFree struct {
gList
n int32
}
gcBgMarkWorker guintptr // (atomic)
gcw gcWork
}
P имеет следующие состояния, участвуют висходный код
государство | ценность | значение |
---|---|---|
_Pidle | 0 | только что был выделен и еще не был инициализирован. |
_Prunning | 1 | Когда M связывает P и вызывает acquirep, состояние P меняется на _Prunning. |
_Psyscall | 2 | Выполняется системный вызов. |
_Pgcstop | 3 | Приостановить операцию, когда система выполняет сборку мусора, и не будет переходить к следующему этапу состояния, пока сборка мусора не завершится. |
_Pdead | 4 | Устарело, больше не используется. |
Внутренняя структура планировщика
Внутренняя структура планировщика, см. полную структуруисходный код
type schedt struct {
lock mutex
midle muintptr // 空闲M链表
nmidle int32 // 空闲M数量
nmidlelocked int32 // 被锁住的M的数量
mnext int64 // 已创建M的数量,以及下一个M ID
maxmcount int32 // 允许创建最大的M数量
nmsys int32 // 不计入死锁的M数量
nmfreed int64 // 累计释放M的数量
pidle puintptr // 空闲的P链表
npidle uint32 // 空闲的P数量
runq gQueue // 全局runnable的G队列
runqsize int32 // 全局runnable的G数量
// Global cache of dead G's.
gFree struct {
lock mutex
stack gList // Gs with stacks
noStack gList // Gs without stacks
n int32
}
// freem is the list of m's waiting to be freed when their
// m.exited is set. Linked through m.freelink.
freem *m
}
Наблюдайте за процессом планирования
Метод трассировки GODEBUG
Переменная GODEBUG может управлять переменными отладки во время выполнения.Параметры разделяются запятыми в формате: имя=значение. Наблюдение за GMP может использовать следующие два параметра:
-
schedtrace: установите параметр schedtrace=X, чтобы среда выполнения выводила строку сводной информации планировщика на стандартный вывод err каждые X миллисекунд.
-
scheddetail: установка schedtrace=X и scheddetail=1 позволяет среде выполнения выводить подробную многострочную информацию каждые миллисекунды X. Информация в основном включает состояние планировщика, процессора, потока ОС и горутины.
package main
import (
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 2000; i++ {
wg.Add(1)
go func() {
a := 0
for i := 0; i < 1e6; i++ {
a += 1
}
wg.Done()
}()
time.Sleep(100 * time.Millisecond)
}
wg.Wait()
}
Выполните следующую команду:
GODEBUG=schedtrace=1000 go run ./test.go
Результат выглядит следующим образом:
SCHED 0ms: gomaxprocs=1 idleprocs=1 threads=4 spinningthreads=0 idlethreads=1 runqueue=0 [0]
SCHED 1001ms: gomaxprocs=1 idleprocs=1 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0]
SCHED 2002ms: gomaxprocs=1 idleprocs=1 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0]
SCHED 3002ms: gomaxprocs=1 idleprocs=1 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0]
SCHED 4003ms: gomaxprocs=1 idleprocs=1 threads=4 spinningthreads=0 idlethreads=2 runqueue=0 [0]
Объяснение содержимого вывода:
- SCHED XXms: SCHED — идентификатор вывода журнала расписания. XXms - время, с которого программа начала выводить текущую строку
- gomaxprocs: число P, равное текущему количеству ядер ЦП, или значение переменной среды GOMAXPROCS.
- idleprocs: количество простаивающих P, отличие от gomaxprocs в количестве работающих P
- threads: количество потоков, то есть количество M
- spinningthreads: количество вращающихся потоков. Когда M не находит горутину, которую можно запланировать для выполнения, поток не будет уничтожен, а будет находиться в состоянии вращения.
- idlethreads: количество бездействующих потоков
- очередь выполнения: количество G в глобальной очереди
- [0]: указывает количество G в локальной очереди P. В скобках P указано несколько чисел.
Метод трассировки инструмента Go
func main() {
// 创建trace文件
f, err := os.Create("trace.out")
if err != nil {
panic(err)
}
defer f.Close()
// 启动trace goroutine
err = trace.Start(f)
if err != nil {
panic(err)
}
defer trace.Stop()
// main
fmt.Println("Hello trace")
}
Выполните следующую команду, чтобы сгенерировать файл трассировки trace.out:
go run test.go
Выполните следующую команду, откройте браузер и откройте консоль для просмотра.
go tool trace trace.out
Суммировать
- Модель многопоточности Golang использует гибридную модель многопоточности, а отношения между потоками и сопрограммами — N:M.
- Модель гибридных потоков Golang реализована с использованием модели GMP для планирования: G — это горутина, сопрограмма, реализованная на golang, M — это поток ОС, а P — логический процессор.
- Каждый M должен быть привязан к P. P имеет локальную исполняемую очередь G, M является единицей, которая выполняет G, и M сначала получает исполняемый процесс G из локальной очереди P. Если он не получен, он будет полученный из других P. Украсть его (то есть worksteal).Если других Ps нет, то они будут получены из глобальной очереди G. Если они не получены, то M будет находиться в крутящемся состоянии и не будет уничтожен .
- При выполнении G, когда происходит блокировка на уровне пользователя, такая как блокировка канала, M не будет блокироваться в это время, и M будет продолжать искать другие работоспособные G. Когда заблокированный G восстановится, он снова войдет в очередь P и ждать выполнения.Если G выполняет систему При вызове, M будет заблокирован.В это время P будет развязан с M (т. е. передача обслуживания), и будет найден новый свободный M. Если свободного нет, будет создан новый M.
- Work Steal и Hand Off обеспечивают эффективное использование потоков.
Эффективными гарантийными стратегиями GMP являются:
- M можно использовать повторно, и его не нужно повторно создавать и уничтожать.Когда нет исполняемой горутины, она находится в состоянии вращения, ожидая пробуждения.
- Стратегии Work Stealing и Hand Off обеспечивают эффективное использование M
- Состояние выделения памяти (mcache) расположено в P, G можно планировать в M, и больше нет проблемы плохой локальности планирования в M.
- M получает G из ассоциированного P, не нуждается в блокировке, не блокируется