Go механизм параллелизма

Go

1. Сравнение «ценностей» в C/C++ и Go

Я уже видел выступление г-на Бай Мина на GopherChina 2017.«Программируй в пути», в котором упоминаются значения трех языков C/C++/Go. Это очень интересно. Позвольте мне поделиться им с вами:

Выдержка из ценностей C

  • Верьте в программистов: предоставляйте указатели и операции с указателями, дайте C-программистам свободу действий
  • Сделай сам, еды и одежды хватит: предоставьте небольшую стандартную библиотеку, а остальное пусть сделает программист
  • Держите язык коротким и простым
  • Приоритет производительности

Выдержка из значения C++

  • Поддерживает несколько парадигм, не заставляя программистов использовать определенную парадигму
  • Не идеально, но практично (и сразу же можно использовать)

Перейти значения

  • Общая простота
  • Ортогональная композиция
  • Предпочтение при параллельных параллельных предпочтениях

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

Из введения ценностей Go видно, что Go очень подходит для параллельного программирования.Можно сказать, что это язык, рожденный для параллелизма.Каков его механизм параллелизма? Это именно то, что эта статья хочет представить.

2. Начиная с модели реализации потока

Существует три основных типа моделей реализации потоков:Модель многопоточности на уровне ядра, модель многопоточности на уровне пользователя и гибридная модель многопоточности. Самая большая разница между ними заключается в соответствии между потоками и объектом планирования ядра KSE (Kernel Scheduling Entity). Так называемая сущность планирования ядра KSE относится к сущности объекта, которая может быть запланирована планировщиком ядра операционной системы, и также вызывается в некоторых местах.потоки уровня ядра, Ядро операционной системы является минимальной единицей планирования.

2.1 Модель потоков на уровне ядра

Пользовательские потоки имеют отношение 1 к 1 с KSE (1:1). Библиотеки потоков большинства языков программирования (например, pthread в Linux, java.lang.Thread в Java, std::thread в C++11 и т. д.) представляют собой слой инкапсуляции потоков операционной системы (потоки на уровне ядра). Каждый созданный поток статически связан с другим KSE, поэтому его планирование полностью выполняется планировщиком ОС. Этот метод прост в реализации, напрямую зависит от возможностей потоков, предоставляемых ОС, и, как правило, не влияет друг на друга между различными пользовательскими потоками. Однако такие операции, как создание, уничтожение и переключение контекста между несколькими потоками, выполняются непосредственно уровнем ОС, что сильно повлияет на производительность ОС в сценариях, требующих большого количества потоков.

2.2 Модель потоков на уровне пользователя

Пользовательские потоки и KSE находятся в отношениях "многие к одному" (M:1). Создание, уничтожение и координация между несколькими потоками выполняются библиотекой потоков, реализованной пользователем, которая прозрачна для ядра ОС. Все потоки созданные в процессе, динамически связываются с одним и тем же KSE во время выполнения. Сейчас существует множество языков, реализующихсопрограммаВ основном это так. По сравнению с потоками на уровне ядра этот метод реализации очень легковесен и потребляет гораздо меньше системных ресурсов, поэтому количество, которое можно создать, и стоимость переключения контекста будут намного меньше. Но у этой модели есть фатальный недостаток: если мы вызываем блокирующий системный вызов в пользовательском потоке (например, чтение сетевого ввода-вывода в блокирующем режиме), то как только KSE запланировано ядром из-за блокировки, все остальные все соответствующие пользовательские потоки блокируются (весь процесс зависает).
Итак, эти языкиБиблиотека сопрограммОн переупаковывает некоторые из своих блокирующих операций в полную неблокирующую форму, а затем активно прекращает работу в точке, которая была ранее заблокирована, и уведомляет или пробуждает другие пользовательские потоки для выполнения каким-либо образом для запуска на KSE. избегает переключения контекста планировщика ядра из-за блокировки KSE, так что весь процесс не будет заблокирован.

2.3 Гибридная многопоточная модель

Пользовательские потоки и KSE находятся в отношениях «многие ко многим» (M:N).Эта реализация сочетает в себе преимущества первых двух моделей, создавая несколько KSE для процесса, а потоки могут динамически связываться с разными KSE во время выполнения. KSE запланировано ядром вне ЦП из-за блокирующей операции потока, работающего над ним, оставшиеся пользовательские потоки, связанные с ним в настоящее время, могут восстановить связь с другими KSE. Конечно, реализация этого механизма динамической ассоциации очень сложна, и пользователи должны реализовать ее сами, что является одним из его недостатков. Параллелизм в языке Go является используемым методом реализации.Для реализации этой модели Go реализует планировщик времени выполнения, отвечающий за динамическую связь между «потоками» в Go и KSE. Эту модель также иногда называютДвухуровневая модель потоков,То есть пользовательский планировщик реализует «планирование» от пользовательских потоков к KSE, а планировщик ядра реализует планирование от KSE к ЦП..

Принципиальные схемы трех моделей выглядят следующим образом:

3. Go Concurrency Scheduling: модель G-P-M

3.1 Модель G-P-M

С вышеизложенным пониманием мы можем начать действительно представлять механизм параллелизма Go.Во-первых, используйте фрагмент кода, чтобы показать, как это выглядит, чтобы создать новый «поток» (называемый Goroutine на языке Go) на языке Go:

// 用go关键字加上一个函数(这里用了匿名函数)
// 调用就做到了在一个新的“线程”并发执行任务
go func() { 
    // do something in one new goroutine
}()

Функционально эквивалентен коду Java8:

new java.lang.Thread(() -> { 
    // do something in one new thread
}).start();

Можно видеть, что параллелизм Go очень прост в использовании, а синтаксический сахар используется для надежной обертки сложной внутренней реализации. Его интерьер можно обозначить следующей картинкой:G, P и M на рисунке — это концепции и объекты структуры данных, абстрагированные от системы времени выполнения языка Go (включая распределитель памяти, параллельный планировщик, сборщик мусора и другие компоненты, которые можно представить как JVM на Java):
G: Аббревиатура для Goroutine.Приведенный выше код с ключевым словом go и вызовом функции создает объект G, который представляет собой инкапсуляцию задачи, которая должна выполняться одновременно, которую также можно назвать потоком пользовательского режима. Это ресурс пользовательского уровня, он прозрачен для ОС, легковесен, может создаваться в больших количествах и имеет низкие затраты на переключение контекста.
M: Аббревиатура от Machine, созданная системным вызовом clone на платформе Linux. Потоки, созданные библиотекой pthread, по существу одинаковы, и все они являются сущностями потоков ОС, созданными системными вызовами. Роль M заключается в выполнении параллельных задач, обернутых в G.Основная обязанность планировщика в системе выполнения Go состоит в том, чтобы справедливо и разумно преобразовать G в несколько M для выполнения.. Он принадлежит к ресурсу ОС, и номер, который может быть создан, также ограничен ОС. Обычно количество G - это больше, чем у активного М.
P: сокращение от Processor, логический процессор, основной функцией которого является управление объектами G (каждый P имеет очередь G) и предоставление локализованных ресурсов для G, работающего на M.
Судя по двухуровневой модели потоков, представленной в разделе 2.3, кажется, что участие P не требуется, достаточно G и M, так зачем добавлять P?
На самом деле, в ранней реализации системы времени выполнения на языке Go (Go1.0) понятия P не было, и планировщик в Go напрямую назначает G соответствующему M для запуска. Однако это приводит к множеству проблем, например, к ресурсам (таким как динамическая память) из системы могут обращаться разные G, когда они одновременно работают на разных M. к конкуренции ресурсов. , чтобы решить аналогичные проблемы, последняя система выполнения Go (Go1.1) добавила P, позволяя P управлять объектом G. Если M хочет запустить G, он должен быть привязан к P перед запуском G под управлением P. . Преимущество этого в том, что мы можем предварительно подать заявку на некоторые системные ресурсы (локальные ресурсы) в объекте P. Когда G в этом нуждается, он сначала обращается к своему локальному P (без защиты блокировки), а если его недостаточно или не относится к глобальному, И когда вы берете его из общей ситуации, вы берете его часть для эффективного использования позже. Точно так же, как когда мы идем к правительству, чтобы что-то сделать, мы сначала идем к местному правительству, чтобы посмотреть, можно ли это сделать.
И поскольку P разделяет объекты G и M, даже если M заблокирован запущенным на нем G, оставшиеся G, связанные с M, могут мигрировать к другим активным M вместе с P и продолжать работать, таким образом, пусть G всегда находит M и запускается вовремя, тем самым улучшая возможности параллелизма системы.
Система выполнения Go реализует систему параллельного планирования пользовательского режима путем создания объектной модели GPM, которая может управлять и планировать свои собственные параллельные задачи, поэтому можно сказать, что язык GoВстроенная поддержка параллелизма.Планировщик, реализованный сам по себе, отвечает за распределение параллельных задач между различными потоками ядра для запуска, а затем планировщик ядра берет на себя выполнение и планирование потоков ядра на ЦП.

3.2 процесс планирования

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

// Goroutine1
func task1() {
    go task2()
    go task3()
}

Если у нас есть G (Горутина1), выполнение которой запланировано на M-P, мы создаем еще две G во время выполнения Горутины1, и эти две G будут немедленно помещены в локальную G того же P, что и Горутина1. В очереди задач очередь ожидает выполнения M, связанного с P. Это самая основная структура, и она хорошо понятна. Ключевые вопросы:
а. Как разумно выделить G к нескольким M, чтобы запустить в многочленной системе, в полной мере использовать многоядерные и улучшать возможности параллелизма?
Если мы пройдем в гуретgoКлючевое слово создает большое количество G, хотя эти G будут временно помещены в ту же очередь, Но если в это время все еще есть незанятые P (количество P в системе равно количеству ядер процессора системы по умолчанию), система выполнения Go всегда может гарантировать наличие хотя бы одного (обычно только одного) активного M, привязанный к простаивающему P к различным G-очередям, чтобы найти выполняемые задачи G, такой тип M называетсяСпин М. Общий порядок поиска таков: очередь P, связанная сама с собой, глобальная очередь, а затем другие P-очереди. Если вы нашли свою P-очередь, вытащите ее и начните запускать, в противном случае перейдите в глобальную очередь, чтобы посмотреть.Так как глобальная очередь нуждается в защите от блокировки, если в ней много задач, пакет будет передан в локальную P-очередь. очередь, чтобы каждый раз не конкурировать за блокировки. Если глобальной очереди до сих пор нет, пора начинать играть по-крупному и красть задачи напрямую из других P-очередей (украсть половину задач обратно). Это гарантирует, что всегда есть комбинации M+P, равные количеству ядер ЦП, когда есть еще G задач, которые могут быть запущены. Во время выполнения задачи G или на пути к задаче G (найти задачу G).
б) Что делать, если определенный М заблокирован системным вызовом в G во время выполнения G?
В этом случае M будет планироваться из ЦП планировщиком ядра и будет находиться в состоянии блокировки, а другие G, связанные с M, не смогут продолжить выполнение, но поток мониторинга (поток sysmon) Система Go runtime может обнаружить такой M, и P, привязанный к M, удаляется, ища другой простаивающий или новый M, чтобы взять на себя P, а затем продолжая запускать в нем G. Общий процесс показан на рисунке ниже. Затем, когда М выйдет из заблокированного состояния, необходимо найти незанятый Р, чтобы продолжить выполнение исходного Г. Если в системе в это время нет неработающего Р, исходный Г помещается в глобальную очередь и ожидает для обнаружения и выполнения других комбинаций M+P.

в) Если определенный G слишком долго работает в M, есть ли способ сделать упреждающее планирование, чтобы другие G в M получали определенное время работы, чтобы обеспечить справедливость системы планирования?
Мы знаем, что планировщик ядра Linux в основном основан на квантах времени и приоритетах планирования. Для потоков с одинаковым приоритетом планировщик ядра попытается обеспечить, чтобы каждый поток мог получить определенное время выполнения. Чтобы предотвратить «голодную смерть» некоторых потоков, планировщик ядра инициирует упреждающее планирование, чтобы прервать длительно выполняющиеся потоки и отказаться от ресурсов ЦП, чтобы позволить другим потокам получить возможности для выполнения. Конечно, в планировщике времени выполнения Go есть аналогичный механизм вытеснения, но нет никакой гарантии, что вытеснение может быть успешным, потому что система времени выполнения Go не имеет возможности прерывания планировщика ядра, его можно установить, только установив параметр Время выполнения G слишком велико. Метод вытеснения флага мягко позволяет работающему G добровольно отказаться от права исполнения M.
Говоря об этом, я должен упомянуть способность Goroutine динамически расширять свой стек потока во время выполнения процесса, он может расширяться от начального размера 2 КБ до максимального размера 1 ГБ (в 64-битной системе), поэтому функцию необходимо вычислить. перед каждым вызовом функции.Объем пространства стека, необходимый для вызова, а затем расширяется по мере необходимости (превышение максимума вызовет исключение во время выполнения). Механизм вытесняющего планирования Go заключается в проверке следующих флагов вытеснения при принятии решения о расширении стека и принятии решения о продолжении выполнения или отказе от него.
Поток мониторинга системы выполнения подсчитывает время и устанавливает флаг вытеснения на G, который выполняется слишком долго. Затем G проверит флаг вытеснения при вызове функции. Если он был установлен, он поместит себя в глобальный очередь, так что M связан с другими G имеют возможность выполнять. Но если выполняемая G является очень трудоемкой операцией без каких-либо вызовов функций (например, просто операция вычисления в цикле for), даже если установлен флаг вытеснения, G будет продолжать занимать текущую M до тех пор, пока не выполнится ее собственная операция. задание выполнено. .

4. Горутины и каналы: еще один механизм синхронизации помимо блокировок

В основных языках программирования для обеспечения безопасности и согласованности данных, совместно используемых несколькими потоками, предоставляется набор базовых инструментов синхронизации, таких как блокировки, условные переменные, атомарные операции и т. д. Неудивительно, что стандартная библиотека языка Go также предоставляет эти механизмы синхронизации, и то, как они используются, аналогично другим языкам.
В дополнение к этим основным методам синхронизации язык Go также предоставляет новый механизм синхронизации: канал, который является базовым типом, таким как int, float32 и т. д. в языке Go.Канал, который передает данные определенного типа. Канал в Go очень близок к BlockingQueue в Java с точки зрения механизма реализации и сценариев использования.

Как пользоваться

// 声明channel变量
var syncChan = make(chan int)  // 无缓冲channel,主要用于两个Goroutine之间建立同步点
var cacheChan = make(chan int, 10)  // 缓冲channel
// 向channel中写入数据
syncChan <- 1
cacheChan <- 1
// 从channel读取数据
var i = <-syncChan
var j = <-cacheChan

Почти эквивалентно операции в Java:

TransferQueue<Integer> syncQueue = new LinkedTransferQueue<Integer>();
BlockingQueue<Integer> cacheQueue = new ArrayBlockingQueue<Integer>(10); 

syncQueue.transfer(1);
cacheQueue.put(1);

int i = syncQueue.take();
int j = cacheQueu.take();

сцены, которые будут использоваться
А. Как и BlockingQueue в Java, он используется в параллельных средах, требующих модели производитель-потребитель.
б) Альтернативный сценарий синхронизации блокировки. В параллельном программировании на Go есть классическая поговорка:Не общайтесь, делясь памятью, но делитесь памятью, общаясь. В Go не рекомендуется использовать блокировки для защиты общего состояния для обмена информацией между различными горутинами (для обмена данными с использованием общей памяти). Вместо этого рекомендуется передавать общее состояние или изменения общего состояния между различными горутинами через канал (для совместного использования памяти посредством связи), что также может гарантировать, что только одна горутина одновременно получает доступ к общему состоянию. как замок. Но это действительно должно изменить способ мышления об использовании блокировок для параллельной синхронизации раньше.Каждый считает, что тот, который подходит им и их сценариям использования, лучше.Нелегко однозначно сказать, какой метод лучше и эффективнее.

5. Go语言对网络IO的优化

Говоря о программировании высокопроизводительного сетевого ввода-вывода, мы почти неотделимы от технических терминов, таких как epoll/kqueue/iocp, таких как новейшие Java NIO, Node и другие высокопроизводительные модели сетевого ввода-вывода, основанные на этих технологиях. Рожденные в 21 веке, естьЯзык C в эпоху ИнтернетаТак называемый язык Go, который придает такое большое значение высокому параллелизму, конечно же, не оставит без внимания оптимизацию сети. И оптимизация сетевого ввода-вывода в языке Go очень умна, так что вы можете программировать с тем же (синхронным) образом мышления, что и раньше (а не античеловеческим асинхронным способом), и вы также можете наслаждаться почти той же эффективностью, что и раньше. асинхронный способ производительность. Как это делается на языке Go? В основном с двух сторон:
А. Инкапсулируйте все сетевые библиотеки в стандартной библиотеке в неблокирующие формы, чтобы они не блокировали базовый M и не вызывали системных издержек, вызванных переключением контекстов планировщиком ядра.
b. Механизм epoll добавлен в систему выполнения (для систем Linux).Когда Горутина выполняет операции сетевого ввода-вывода, если сетевой ввод-вывод не готов, Горутина будет инкапсулирована и помещена в очередь ожидания epoll.Текущий G зависает. С этого момента связанный с ним M может продолжать запускать другие G. Когда соответствующий сетевой ввод-вывод будет готов, система выполнения Go примет G, ожидающий готовности сетевого ввода-вывода, из очереди готовности epoll (в основном в двух местах, чтобы получить список G готового сетевого ввода-вывода из epoll, одно находится в потоке мониторинга sysmon , второй — спин M), а затем планировщик назначает их каждому M для выполнения, как обычный G.
Язык Go напрямую интегрирует реализацию высокопроизводительного сетевого ввода-вывода в систему выполнения самой Go и эффективно работает с параллельной системой планирования Go, позволяя разработчикам выполнять сетевое программирование просто и эффективно.

6. Суммировать

Go не идеален, этоЯзыковой дизайн для разработки программного обеспечения. Механизм параллелизма, который он реализует, не является инновационной технологией, он просто сочетает эти классические теории и технологии в сжатой и эффективной форме и открывает его для разработчиков с помощью простых и абстрактных API или синтаксического сахара, что действительно облегчает разработчиков. . Более того, введение механизма канала дает нам еще одну модель параллельного программирования (CSP: Communication Sequential Process) и дает нам возможность использовать другие методы мышления параллельного программирования (для модели CSP рекомендуется взглянуть на в «Seven Weeks Seven», шестая глава книги «Модель параллелизма»), комбинация Goroutine и Channel представляет собой пару мощных параллельных инструментов, я считаю, что она может принести вам лучший опыт параллельного программирования.

7. Ссылаться на

«Перейти к параллельному программированию в действии», 2-е издание
«Иди языковые учебы»
Также поговорим о планировщике goroutine
Go coding in go way