Пусть люди, которых я встречу, вещи, которые я испытаю, даже если я стану немного лучше, я буду удовлетворен.
Эта статья воспроизведена из:blog.horn99.org/post/go wave…
1. Информация о видео
1. Адрес просмотра видео
Woohoo.YouTube.com/watch?V=KB Z…
2, скачать РРТ
download.CSDN.net/download/ Сюй ...
3. Блог Post.
about.source graph.com/go/under это та…
2. Параллелизм в Go
- горутины: выполнять каждую задачу независимо, возможно, параллельно
- каналы: для связи и синхронизации между горутинами
1. Пример простой обработки транзакций
Для непараллельной программы, подобной этой:
func main() {
tasks := getTasks()
// 处理每个任务
for _, task := range tasks {
process(task)
}
}
Преобразовать это в шаблон параллелизма Go легко, используя типичный шаблон очереди задач:
func main() {
// 创建带缓冲的 channel
ch := make(chan Task, 3)
// 运行固定数量的 workers
for i := 0; i < numWorkers; i++ {
go worker(ch)
}
// 发送任务到 workers
hellaTasks := getTasks()
for _, task := range hellaTasks {
ch <- task
}
...
}
func worker(ch chan Task) {
for {
// 接收任务
task := <-ch
process(task)
}
}
2. Характеристики каналов
- goroutine-safe, несколько goroutine могут получить доступ к каналу одновременно.
- Может использоваться для хранения и передачи значений между горутинами
- Его семантика — «первым пришел — первым вышел» (FIFO).
- Может заставить горутину блокировать и разблокировать
В-третьих, решить
1. Построить канал
// 带缓冲的 channel
ch := make(chan Task, 3)
// 无缓冲的 channel
ch := make(chan Task)
Просмотрите характеристики упомянутых ранее каналов, особенно первых двух. Что бы вы сделали, если бы проигнорировали встроенные каналы и позволили бы вам спроектировать что-то, что было бы безопасным для горутин и могло использоваться для хранения и передачи значений? Многие люди могут подумать, что это возможно сделать с помощью очереди с замком. Да, по сути, канал — это очередь с замком внутри.gowave.org/двуспальная кровать/runtime…
type hchan struct {
...
buf unsafe.Pointer // 指向一个环形队列
...
sendx uint // 发送 index
recvx uint // 接收 index
...
lock mutex // 互斥量
}
Конкретная реализация buf очень проста, то есть реализация циклической очереди. sendx и recvx используются для записи местоположения отправки и получения соответственно. Затем используйте мьютекс блокировки, чтобы исключить риск конкуренции.
Для каждой операции типа ch := make(chan Task, 3) в куче выделяется место, создается и инициализируется структурная переменная hchan, а ch является указателем на эту структуру hchan.
Поскольку ch сам по себе является указателем, мы можем напрямую передать ch при вызове функции горутины, вместо того, чтобы брать указатель с &ch, все горутины, использующие один и тот же ch, указывают на одно и то же фактическое пространство памяти.
2. Отправьте и получите
Для удобства описания мы используем G1 для представления горутины функции main(), а G2 — для представления горутины рабочего процесса.
// G1
func main() {
...
for _, task := range tasks {
ch <- task
}
...
}
// G2
func worker(ch chan Task) {
for {
task :=<-ch
process(task)
}
}
2.1 Простая отправка и получение
Итак, что именно делает ch
- получить замок
- enqueue(task0) (здесь задача копирования памяти)
- разблокировать замок
Этот шаг очень прост, давайте посмотрим G2, T: =
- получить замок
- t = dequeue() (опять же, здесь копия памяти)
- разблокировать замок
Этот шаг также очень прост. Но из этой операции видно, что единственной частью, общей для всех горутин, является структура этого hchan, а все коммуникационные данные — это копии памяти. Это следует основной концепции параллелизма в Go:
Do not communicate by sharing memory;instead, share memory by communicating
Копия памяти относится к:
// typedmemmove copies a value of type t to dst from src.
// Must be nosplit, see #16026.
//go:nosplit
func typedmemmove(typ *_type, dst, src unsafe.Pointer) {
if typ.kind&kindNoPointers == 0 {
bulkBarrierPreWrite(uintptr(dst), uintptr(src), typ.size)
}
// There's a race here: if some other goroutine can write to
// src, it may change some pointer in src after we've
// performed the write barrier but before we perform the
// memory copy. This safe because the write performed by that
// other goroutine must also be accompanied by a write
// barrier, so at worst we've unnecessarily greyed the old
// pointer that was in src.
memmove(dst, src, typ.size)
if writeBarrier.cgo {
cgoCheckMemmove(typ, dst, src, 0, typ.size)
}
}
3. Блокировка и восстановление
3.1 Отправитель заблокирован
Предположим, что G2 обрабатывается долго, в течение которого G1 продолжает отправлять задачи:
ch <- task1
ch <- task2
ch <- task3
Но когда ch
3.2 Планирование горутин во время выполнения
Во-первых, горутины — это не потоки операционной системы, а потоки пользовательского пространства. Следовательно, горутины создаются и управляются средой выполнения Go, а не ОС, поэтому они легче, чем потоки ОС.
Конечно, горутина в конечном итоге будет выполняться в потоке, и именно планировщик в среде выполнения Go контролирует, как горутина выполняется в потоке.
Планировщик времени выполнения Go представляет собой модель планирования M:N, где N горутин выполняются в потоках M OS. Другими словами, в потоке ОС может выполняться несколько горутин.
В планировании M:N в Go используются 3 структуры:
- М: поток ОС
- G: goroutine
- P: контекст планирования
- P имеет очередь выполнения всех горутин и их контекстов.
3.3 Конкретный процесс блокирования горутины
Затем, когда выполняется ch
- G1 вызовет gopark во время выполнения
- Затем планировщик времени выполнения Go берет на себя
- Установите состояние G1 на ожидание
- Отношения между G1 и M отключены (отключены), поэтому G1 отключен от M, другими словами, M свободен и может быть запланирован для других задач.
- Получить работающую горутину G из очереди выполнения P
- Создайте новую связь между G и M (Switch in), чтобы G был готов к работе.
- При возврате планировщика запустится новый G, а не запустится G1, то есть блок.
Как видно из приведенного выше процесса, для горутины блокируется G1 и начинает выполняться новая G, а для потока операционной системы M он вообще не блокируется.
Мы знаем, что потоки ОС намного тяжелее, чем горутины, поэтому старайтесь избегать здесь блокировки потоков ОС, что может повысить производительность.
3.4 горутина реализации процесса восстановления
Вы разобрались с блокировкой ранее, так что давайте разберемся, как возобновить работу. Однако, прежде чем продолжить понимать, как восстанавливать, нам нужно глубже понять структуру hchan. Потому что, когда канал не заполнен, как планировщик узнает, какую горутину продолжать выполнять? И как горутина узнает, откуда брать данные?
В hchan, в дополнение к упомянутому выше содержимому, определены две очереди, sendq и recvq, которые представляют горутины, ожидающие отправки и получения, и связанную с ними информацию.
type hchan struct {
...
buf unsafe.Pointer // 指向一个环形队列
...
sendq waitq // 等待发送的队列
recvq waitq // 等待接收的队列
...
lock mutex // 互斥量
}
где waitq — очередь со структурой связанного списка, каждый элемент — структура sudog, которая примерно определяется как:
type sudog struct {
g *g // 正在等候的 goroutine
elem unsafe.Pointer // 指向需要接收、发送的元素
...
}
gowave.org/двуспальная кровать/runtime…
Итак, в предыдущем процессе блокировки G1, по сути:
- G1 создаст для себя переменную sudog
- Затем добавьте в очередь ожидания SendQ, чтобы облегчить восстановление G1 в будущем Receiver.
Все это происходит до вызова планировщика.
Итак, давайте начнем смотреть, как восстановиться сейчас.
Когда G2 вызывает t :=
- G2 сначала выполняет dequeue(), чтобы получить задачу1 из буферной очереди в t
- G2 выдает ожидающий sudog из sendq
- enqueue() значение elem во всплывающем окне sudog для buf
- Измените Goroutine во всплывающем окне SUDOG, который является G1, от ожидания Runnable
- Затем G2 должен уведомить планировщик о том, что G1 готов к планированию, поэтому вызывается goready(G1).
- Планировщик изменяет состояние G1 на работоспособное.
- Планировщик помещает G1 в очередь выполнения P, поэтому при планировании в какой-то момент в будущем G1 снова начнет выполняться.
- Назад к G2
Обратите внимание, что G2 отвечает за помещение элемента G1 в buf, что является оптимизацией. Таким образом, когда G1 возобновит работу в будущем, нет необходимости запрашивать блокировку, enqueue() и снова снимать блокировку. Это позволяет избежать накладных расходов на несколько блокировок.
3.5 Если сначала блокируется прием?
Что еще круче, так это процесс, при котором получатель блокируется первым.
Если G2 сначала выполнит t :=
- G2 создает для себя структурную переменную sudog. где g — это сама G2, а elem указывает на t
- Поместите эту переменную sudog в очередь ожидания recvq
- G2 должен сообщить горутине, что ему нужно сделать паузу, поэтому вызовите gopark(G2)
- Как и прежде, планировщик меняет состояние своего G2 на ожидание.
- Отсоедините G2 и M
- Удалить горутину из очереди выполнения в P
- Установите связь между новой горутиной и M
- Возврат и запуск новой горутины
Они должны быть знакомы, так как же выглядит процесс, когда G1 начинает отправлять данные?
G1 может поставить в очередь (задачу), а затем вызвать goready (G2). Однако мы можем быть умнее.
По состоянию структуры hchan мы уже знаем, что после входа задачи в buf после возобновления работы G2 она прочитает его значение и скопирует в t. Тогда G1 может вообще не ходить в buf, а G1 может напрямую отправлять данные в G2.
Горутины обычно имеют свои собственные стеки, и друг у друга не будет доступа к данным стека друг друга, кроме каналов. Здесь, поскольку мы уже знаем адрес t (через указатель elem) и поскольку G2 не запущен, мы можем безопасно назначить его напрямую. Когда G2 возобновляет работу, ему не нужно снова запрашивать блокировку или работать с buf. Это экономит накладные расходы на операции копирования и блокировки памяти.
4. Резюме
- goroutine-safe
- заблокировать мьютекс в hchan
- Хранить, передать значение, FIFO
- Реализовано через кольцевой буфер в хчане
- Заставляет горутину блокировать и возобновлять
- sendq и recvq в hchan, то есть очередь связанного списка структуры sudog
- Вызовите планировщик времени выполнения (Gopark (), Goready ())
В-четвертых, работа других каналов
1. Небуферизованный канал
Небуферизованный канал ведет себя так же, как описанный ранее пример прямой отправки:
- Получатель блокирует → отправитель записывает непосредственно в стек получателя
- блокировка отправителя → метод accept читает непосредственно из sudog отправителя
2. выберите
gowave.org/двуспальная кровать/runtime…
- Сначала заблокируйте все каналы, которые необходимо использовать.
- Создайте для себя sudog, затем добавьте sendq или recvq на все каналы (в зависимости от отправки или получения)
- Разблокируйте все каналы, затем приостановите горутину, которая в данный момент вызывает select (gopark()).
- Затем, когда какой-либо канал доступен, горутина выбора будет запланирована для выполнения.
- resuming mirrors the pause sequence
5. Почему Go разработан таким образом?
1. Простота Предпочитайте блокирующие очереди реализациям без блокировок.
Улучшения производительности не пустые, которые увеличиваются с увеличением сложности.
Двиков Последнее может быть лучше, но это преимущество не обязательно может преодолеть недостатки сложности кода реализации.
2. Производительность
- Вызовите планировщик среды выполнения Go, чтобы потоки ОС не блокировались при чтении и записи в стеках горутин.
- Это позволяет горутине просыпаться без необходимости получения блокировки.
- Некоторых копий памяти можно избежать.
Конечно, любое преимущество имеет свою цену. Цена здесь — сложность реализации, поэтому существуют более сложные механизмы управления памятью, сборка мусора и механизмы сжатия стека.
Преимущество повышения производительности здесь больше, чем недостаток повышенной сложности.
Таким образом, в различных кодах реализации канала мы можем увидеть результат этого компромисса простоты Performance.
Шесть, информация о блоггерах
Личный публичный аккаунт WeChat: