Оглавление

  1. Фундаментальные концепции
  2. Взаимодействие с ОС и I/O
  3. Go Runtime и модель GMP
  4. Примитивы синхронизации
  5. Каналы (Channels)
  6. Оператор Select и Context
  7. Обработка сигналов и завершение
  8. Проблемы многопоточности

1. Фундаментальные концепции

Конкурентность vs Параллелизм

По определению Роба Пайка:

  • Конкурентность (Concurrency): Композиция независимо выполняющихся процессов. Это про структуру программы. Способность решать несколько задач одновременно, переключаясь между ними.
  • Параллелизм (Parallelism): Одновременное выполнение вычислений. Это про исполнение. Невозможен без многоядерной архитектуры.

Модели многозадачности

  • 1:1 (Один к одному): Один поток ОС на одну задачу.
    • Плюсы: Простота, исполнение на уровне ядра.
    • Минусы: Дорогое создание потоков, накладные расходы.
  • 1:N (Один ко многим): Один поток ОС на множество задач.
    • Плюсы: Эффективно для IO-задач.
    • Минусы: Блокировка одной задачи блокирует весь поток ОС. Нет параллелизма.
  • M:N (Многие ко многим): Множество задач на множество потоков ОС. Используется в Go.
    • Плюсы: Гибридная модель, эффективное использование ресурсов.

Виды многозадачности

  • Кооперативная: Задачи добровольно передают управление (риск блокировки всей системы зависшей задачей).
  • Вытесняющая: Система сама прерывает задачи по кванту времени.
  • В Go: Гибридная модель (кооперативная с элементами вытеснения через sysmon).

2. Взаимодействие с ОС и I/O

User Space vs Kernel Space

  • User space: Изолированная область для приложений. Ограничен доступ к ресурсам.
  • Kernel space: Область ядра ОС. Полный доступ к оборудованию.
  • Системные вызовы (Syscalls): Способ обращения приложения из user space к функциям ядра.
  • Context Switch: Дорогостоящий процесс сохранения/восстановления состояния потока при переключении между ними или при переходе в режим ядра.

Мультиплексированный ввод-вывод

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

  • select(): Устаревший механизм (лимит 1024 дескриптора, медленный перебор).
  • epoll (Linux) / kqueue (BSD): Современные механизмы для решения проблемы C10K. Позволяют эффективно обслуживать десятки тысяч соединений. Используются в Go Netpoller.

3. Go Runtime и модель GMP

Компоненты Runtime

Go-приложения имеют большой рантайм (МБ), который включает:

  • Scheduler: Управление горутинами.
  • GC: Сборщик мусора.
  • Netpoller: Асинхронный сетевой ввод-вывод.
  • Sysmon: Системный мониторинг (фоновая проверка).

Модель GMP (M:N)

  • G (Goroutine): Логический поток. Начальный стек 2KB (динамический).
  • M (Machine): Поток ОС (thread). Выполняет код.
  • P (Processor): Контекст планирования. Количество P = количеству ядер (GOMAXPROCS). Связывает G и M.

Очереди:

  • LRQ (Local Run Queue): Локальная очередь у каждого P.
  • GRQ (Global Run Queue): Глобальная очередь для “лишних” горутин.

Механизмы планировщика (Work-stealing, Handoff)

Раунд планирования:

  1. 1/61 времени — проверка GRQ.
  2. Проверка собственной LRQ.
  3. Work-stealing: Кража половины задач из LRQ другого процессора P (если своя пуста).
  4. Проверка Netpoller.

«Тик» в Go

  • Тик — это внутреннее событие планировщика Go, когда M (поток ОС) проверяет и запускает горутину.
  • Связка терминов:

    • G — goroutine
    • P — виртуальный процессор (scheduler context)
    • M — машина/поток ОС, выполняющий G через P

«Every 61 ticks» в планировщике

  • Каждые 61 тик M проверяет глобальную очередь готовых к запуску горутин (global run queue).
  • Цель:

    • Обеспечить справедливое распределение работы между P
    • Избежать голодания горутин
  • Локальная очередь runq у каждого P используется чаще, глобальная — реже.

Пример кода из runtime Go:

if schedtick%61 == 0 && sched.runqsize > 0 {
    gp = globrunqget(_p_)
    if gp != nil {
        return gp, false
    }
}

Почему выбрано число 61?

  • Эмпирически оптимизировано для баланса производительность ↔ fairness.
  • Проверка глобальной очереди каждый тик — дорого, слишком редкая проверка — плохо для справедливости.
  • 61 — простое число:

    • Снижает вероятность синхронизации циклов между потоками/виртуальными процессорами
    • Уменьшает lock contention на глобальную очередь

Простые числа и распределение нагрузки

  • Если бы использовалось составное число (например, 60), циклы нескольких P могли бы совпадать, вызывая пиковую нагрузку на lock.
  • Простое число «размывает» циклы, делая проверку очередей более равномерной.
  • Используется как псевдослучайный распределитель, повышающий масштабируемость.

Lock contention

  • Lock contention — конкуренция потоков/горутин за один и тот же lock.

  • Проблемы:
    • Потоки ждут, пока lock освободится → падение производительности
    • Ухудшение масштабируемости при росте числа потоков
  • Методы снижения lock contention:
    1. Сокращать критическую секцию (короткий Lock/Unlock)
    2. Использовать локальные очереди (P.runq) вместо глобальной
    3. Атомарные операции (sync/atomic)
    4. Шардирование ресурсов
  • В Go runtime:
    • Локальные очереди минимизируют lock contention
    • Проверка глобальной очереди раз в 61 тик уменьшает «толпу» потоков вокруг одного lock

Итог

  • 1/61 тик = каждые 61 планировочный цикл M проверяет глобальную очередь, чтобы:
    • Обеспечить fairness
    • Снизить lock contention
    • Поддерживать производительность при высокой конкуренции
  • Простое число помогает равномерно распределять проверки между потоками, предотвращая пики нагрузки.

Handoff (Передача):

  • Если горутина делает блокирующий системный вызов (syscall), она блокирует поток M.
  • Процессор P отсоединяется от M и находит/создает новый поток M для продолжения работы над оставшимися горутинами из своей LRQ.

Sysmon:

  • Опрашивает Netpoller каждые 10 мс.
  • Принудительно помечает горутины, работающие > 10 мс, для вытеснения.

Потоки ОС vs Горутины

Характеристика Потоки ОС Горутины
Управление Ядро ОС Go runtime
Переключение ~1-2 мкс ~200 нс
Стек Фиксированный (1-8 MB) Динамический (от 2 KB)

4. Примитивы синхронизации

Пакет sync

  • WaitGroup: Ожидание группы горутин.
  • Mutex: Взаимное исключение (один Lock в момент времени).
  • RWMutex: Много читателей (RLock) ИЛИ один писатель (Lock).
  • Once: Однократное выполнение кода.
  • Atomic: Атомарные операции без прерываний (счетчики).

Семафоры и Мьютексы

  • Семафор: Счетчик доступных ресурсов (например, ограничение пула соединений).
  • Мьютекс: Взаимное исключение для одного ресурса (критическая секция).
    • Пример: Мьютекс защищает структуру данных от гонок, Семафор лимитирует количество потоков в БД.

5. Каналы (Channels)

Философия и свойства

“Don’t communicate by sharing memory, share memory by communicating.”

  • IPC: Реализуют межпроцессное взаимодействие внутри приложения.
  • FIFO: Строгая очередность.
  • Потокобезопасность: Встроенная блокировка и синхронизация.
  • Буферизация:
    • Небуферизированные: Рандеву (синхронизация) читателя и писателя.
    • Буферизированные: Блокировка только при пустом/полном буфере.

Операции и состояния (Таблица поведения)

Операция nil канал Закрытый канал Открытый канал
close panic panic success
send (<-) block forever panic block или success
recv (<-) block forever never block (zero value) block или success

Внутреннее устройство (hchan)

Канал — это структура в куче:

  • buf: Кольцевой буфер данных.
  • lock: Мьютекс для защиты канала.
  • recvq / sendq: Списки ожидания (очереди горутин).
  • Direct Write: Оптимизация — копирование данных из стека писателя в стек читателя напрямую, минуя буфер, если читатель уже ждет.

6. Оператор Select и Context

Работа Select

  • Позволяет ждать готовности множества каналов.
  • Если готовы несколько — выбор псевдослучайный.
  • default: Делает операцию неблокирующей. Без него — блокировка до готовности любого case.
  • Ошибка: Чтение из закрытого канала в select всегда готово (вернет zero value), что может породить бесконечный цикл.

Контекст (context)

Механизм управления деревом вызовов.

  • Отмена каскадная: Отмена родителя отменяет всех детей.
  • Основные виды:
    • Background() / TODO(): Базовые контексты.
    • WithCancel(): Ручная отмена.
    • WithTimeout() / WithDeadline(): Отмена по времени.
    • WithValue(): Передача метаданных (использовать с осторожностью).
  • Done(): Канал для отслеживания завершения.

7. Обработка сигналов и завершение

Graceful Shutdown (Корректное завершение):

  1. Создание буферизированного канала для os.Signal.
  2. Подписка через signal.Notify (на SIGINT, SIGTERM).
  3. Ожидание сигнала и вызов cancel() контекста.
  4. Важно: SIGKILL перехватить нельзя.

8. Проблемы многопоточности

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

  • Deadlock: Взаимная блокировка (например, чтение из nil канала или циклическая зависимость).
  • Livelock: Бесполезная активность без прогресса.
  • Lock contention: Высокая конкуренция за одну блокировку, снижающая производительность.
  • Утечки горутин: Горутины, заблокированные навсегда (например, в select без таймаута или на чтении из канала, который никто не закроет).