За кулисами среды выполнения Go

Пока не беспокойтесь о понимании изображения выше, мы начнем с основ.

Горутины распределяются по нескольким потокам, которые управляются планировщиком Go под капотом. О горутинах мы знаем следующее:

  • Горутины не обязательно быстрее потоков с точки зрения скорости выполнения, поскольку для их выполнения требуются потоки.

  • Основное преимущество горутин — такие нюансы, как контекст переключения, объем памяти, которую они занимают, а также стоимость создания и «удаления».

Вы, наверное, уже слышали о планировщике Go, но что мы знаем о том, как он работает? Как он соединяет горутины с потоками?

Давайте рассмотрим операции, которые выполняет планировщик поочередно.

1. Планировщик М:Н

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

go doWork()

Но за этим простым шагом скрывается гораздо более сложная система.

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

Перейти к планировщику

Перейти к планировщику

А что насчет М:Н?

Это означает, что роль планировщика Go состоит в том, чтобы связать M горутин с N потоками ядра, формируя таким образом модель M:N. У вас может быть либо больше потоков ОС, чем ядер, либо больше горутин, чем потоков.

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

  • Конкурентоспособность: предполагает одновременное выполнение нескольких задач, но не обязательно одновременно.

  • Параллелизм: Это означает, что одновременно выполняются несколько задач, скорее всего, с использованием нескольких ядер ЦП.

Конкурентоспособность против. Параллелизм

Конкурентоспособность против. Параллелизм

Давайте посмотрим, как планировщик Go обрабатывает потоки.

2. Модель ПМГ

Прежде чем раскрыть внутреннюю работу, давайте разберемся, что такое P, M и G.

G (Горутина)

Горутина — это наименьшая единица выполнения в Go, которая работает как легкий поток.

В среде выполнения Go горутина представляет собой структуру. g. Как только он вызывается, он находит свое место в локальной очереди выполнения логического процессора П. П, в свою очередь, передает его на исполнение потоку операционной системы (М).

Горутина может находиться в трёх (основных) состояниях:

  • В ожидании: В этом состоянии горутина неактивна. Например, он прерывается при операциях с каналами или блокировками или может быть остановлен системным вызовом.

  • Исполняемый файл: Горутина готова к запуску, но еще не выполнена. Она ждет своей очереди на стрим (M).

  • Бег: Горутина выполняется в потоке (M). Это будет продолжаться до тех пор, пока задание не будет завершено, или пока оно не будет прервано планировщиком, или пока что-то еще не заблокирует его.

ЧИТАТЬ   Тимоти Шаламе и Кайли Дженнер впервые увидели вместе
Горутины состояния

Горутины состояния

Горутины НЕТ использовали один раз, потом выбросили.

Когда запускается новая горутина, планировщик Go обращается к пулу горутин, чтобы получить ее, а если ее нет, он создает новую. Эта новая горутина добавляется в очередь выполнения процессора (P).

P (логический процессор)

В планировщике Go, когда мы говорим о «процессоре», мы имеем в виду логический объект, а не физический процессор.

Однако по умолчанию для счетчика P установлено количество ядер, доступных на хосте. Вы можете изменить это значение, используя runtime.GOMAXPROCS(int).

runtime.GOMAXPROCS(0) // получить доступное количество логических ядер на хосте
// Output: 8 (зависит от процессора)

Если вы планируете изменить это значение, лучше всего сделать это один раз при запуске приложения. Если вы измените его во время выполнения, это приведет к STW (stopTheWorld), все приложение приостанавливается, пока изменяется количество процессоров.

Каждый P содержит свой собственный список исполняемых горутин, называемый локальной очередью выполнения, размер которой может достигать 256 горутин.

cbe28477edd54bd0303a6dadeafcb366 Логический процессор P» title=»Планировщик -> Логический процессор P» ширина=»815″ высота=»456″ источник данных=»https://habrastorage.org/getpro/habr/upload_files/cbe/284/77e/cbe28477edd54bd0303a6dadeafcb366.png»/>

Планировщик -> Логический процессор P

Если очередь P достигает максимального числа (256) и переполняется, возникает глобальная очередь выполнения, но мы доберемся до нее чуть позже.

«Так о чем же нам говорит число П? »

Число P говорит нам, сколько горутин может выполняться одновременно.

М (тема ОС)

Типичное приложение Go может использовать до 10 000 потоков.

И да, мы говорим о потоках, а не о горутинах. Если вы превысите этот лимит, приложение может аварийно завершить работу.

«Когда создается тред? »

Подумайте об этой ситуации: горутина находится в исполняемом состоянии и требует потока.

Что произойдет, если все потоки уже заблокированы, возможно, из-за системных вызовов или невытесняемых операций? В этом случае в дело вступает планировщик и создаёт новый поток для этой горутины.

(Обратите внимание: если поток просто занят сложным вычислением или задачей с большим временем выполнения, это не означает, что он будет считаться заблокированным)

Если вы хотите изменить ограничение скорости по умолчанию, вы можете использовать функцию runtime/debug.SetMaxThreads() . Это позволит вам установить максимальное количество потоков операционной системы, которые может использовать приложение.

ЧИТАТЬ   Станьте зависимым от МЯСА с этим рецептом! БОМБИЧЕСКИ вкусно и ТАК ПРОСТО! Азу – это НЕ классический рецепт!

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

3. Как работает MPG

Давайте шаг за шагом разберемся, как M, P и G работают вместе.

Не буду вдаваться во все мелкие детали, но в общих чертах это работает так:

Как работает планировщик Go

Как работает планировщик Go

  1. Запускаем горутину: с использованием go func()Go Runtime создает новую горутину или использует существующую из пула.

  2. Расположение в очереди: Горутина ищет место в локальных очередях логических процессоров (P), и если они все заполнены, она помещается в глобальную очередь.

  3. Ссылка на канал: на этом этапе в игру вступает поток (M), который принимает P и начинает выполнять горутины из своей локальной очереди. Как только поток начинает выполнять горутину, процессор (P), в очереди которого была горутина, связывается с этим потоком (M) и становится недоступным для других потоков.

  4. «Кража работы»: если локальная очередь процессора (P) пуста, поток M пытается заимствовать половину исполняемых горутин из локальной очереди другого процессора (P). Если ничего не найдено, поток (M) проверяет глобальную очередь, а затем Net Poller (схема процесса приведена ниже). рабочее воровство).

  5. Распределение ресурсов : Как только поток M выбирает горутину G, он предоставляет ей все необходимые ресурсы.

«А заблокированные потоки? »

Если горутина выполняет системный вызов, который займет много времени (например, чтение файла), поток M будет ждать.

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

Заблокированные обсуждения

Заблокированные обсуждения

Процесс кражи труда

Когда поток M завершил все свои задачи и ему больше нечего делать, это не означает, что он будет простаивать.

Поток будет искать горутины в локальных очередях других логических процессоров и забирать половину их горутин:

Процесс заимствования

Процесс заимствования

  1. Каждые 61 такт поток M проверяет глобальную очередь, и если она содержит исполняемую горутину, воровство работы прекращается.

  2. Поток M ищет исполняемые горутины в локальной очереди, связанной с его процессором P.

  3. Если поток ничего не находит в локальной очереди, он снова проверяет глобальную очередь.

  4. Поток проверяет сетевой опросчик на наличие ожидающих горутин, работающих в сети.

  5. Если поток после проверки сетевого опросчика по-прежнему не нашел ни одной задачи, он перейдет в активный режим циклического поиска.

  6. В этом состоянии поток попытается заимствовать горутины из локальных очередей других процессоров.

  7. Если после всех этих действий поток по-прежнему не нашел никакой работы, он прекращает активный поиск.

  8. Теперь, если появляются новые горутины и есть свободный CPU без ассоциированного потока, к работе можно привлечь другой поток.

ЧИТАТЬ   Уровень реки Тобол возле Кургана упал на 20 сантиметров

Обратите внимание, что глобальная очередь проверяется дважды: один раз каждые 61 тик и если локальная очередь пуста.

«Если M связано с его P, как он может брать горутины из другого процессора? Перемещается ли поток от одного процессора к другому?

Ответ — нет.

Даже если поток M возьмет горутины из несвязанного процессора P, он выполнит эти горутины, используя свой процессор. Таким образом, пока поток выполняет чужие горутины, он не теряет связи со своим процессором.

«Почему 61? »

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

Это может снизить риск возникновения закономерностей, которые в конечном итоге приведут к коллизиям или другому нежелательному поведению.

Если вы будете проверять чаще, это может занять слишком много ресурсов; если вы будете проверять реже, горутины могут ждать слишком долго для выполнения.

Сетевые терминалы

Мы не затрагивали тему опроса сети, но упомянули ее в диаграмме перехвата работы.

Как и планировщик Go, Network Poller является компонентом среды выполнения Go и используется для выполнения сетевых запросов, таких как операции сетевого ввода-вывода.

Существует 2 типа системных вызовов:

  • Системные вызовы, связанные с сетью: Когда горутина выполняет сетевую операцию ввода-вывода, вместо блокировки потока она добавляется в обозреватель сети. Netpoller ожидает асинхронного выполнения операции, и когда он ждет, горутина снова становится исполняемой, и ее выполнение становится доступным для потока.

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

примерно. Network Poller — это поток Go, который реализует мультиплексирование сетевых операций ввода-вывода. Например, в Linux эта концепция реализована с помощью инструмента epoll.

Source

От admin