Оглавление

  1. Основы конкурентности
  2. Горутины (Goroutines)
  3. Состояние гонки и синхронизация (Пакет sync)
  4. Каналы (Channels)
  5. Итоги и перспективы

1. Основы конкурентности

Процессы и потоки

Конкурентное программирование базируется на примитивах операционной системы (ОС):

  • Процесс (Process):
    • Примитив изоляции ресурсов: ОС выделяет процессу память (RAM) и файловые дескрипторы.
    • Память одного процесса недоступна другому (безопасность и стабильность).
  • Поток (Thread):
    • Примитив исполнения: “Контейнер” для последовательных инструкций внутри процесса.
    • Обладает собственным локальным стеком для переменных.
    • Имеет доступ к общей памяти процесса, разделяемой с другими его потоками (риск конфликтов).
    • Переключение контекста (Context Switch): Механизм ОС для переключения между потоками. Это ресурсоемкая (дорогая) операция.

Модели выполнения кода

  • Синхронное выполнение: Инструкции выполняются строго последовательно (один CPU, один поток).
  • Параллельное выполнение (Parallelism): Физическое одновременное выполнение задач на нескольких ядрах CPU для ускорения вычислений.
  • Асинхронное выполнение (Asynchrony): Эффективное использование CPU во время ожидания I/O-операций (сеть, диск). Планировщик переключает поток на другую задачу, пока ждет ответ.

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

  • Конкурентность (Concurrency): Подход к проектированию программы как набора независимых частей (сопрограмм).
  • Параллелизм: Частный случай реализации конкурентности — одновременное физическое исполнение задач.
    • Фундаментальная мысль: Конкурентность — это про структуру; параллелизм — это про исполнение.

2. Горутины (Goroutines)

Определение и запуск

  • Горутина: Легковесный “логический поток”, управляемый средой выполнения Go (Go Runtime), а не ОС.
  • Реализация: Является надстройкой над системными потоками. Планировщик Go распределяет множество горутин по пулу реальных потоков.
  • Запуск: Ключевое слово go перед вызовом функции: go myFunction().
  • Преимущества: Создание и переключение контекста горутин значительно дешевле и быстрее, чем у системных потоков.

Жизненный цикл

  • Главная горутина: Функция main выполняется в корневой горутине.
  • Завершение: При выходе из main весь процесс завершается немедленно. Все дочерние горутины принудительно останавливаются, программа не ждет их окончания.
  • Связь: Между “родительской” и “дочерней” горутиной нет жесткой иерархической зависимости после запуска.

3. Состояние гонки и синхронизация (Пакет sync)

Data Race (Состояние гонки)

  • Определение: Ситуация, когда несколько горутин одновременно обращаются к одной области памяти и хотя бы одна выполняет запись.
  • Последствия: Непредсказуемый результат, меняющийся от запуска к запуску. Операция ++ (инкремент) не является атомарной и подвержена гонкам.
  • Детектор гонок: Инструмент отладки, запускаемый флагом -race:
    go run -race main.go
    

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

Для защиты критических секций и управления горутинами используется пакет sync:

  • sync.WaitGroup:
    • Назначение: Ожидание завершения группы горутин.
    • Add(n): Увеличение счетчика задач.
    • Done(): Уменьшение счетчика (рекомендуется вызывать через defer).
    • Wait(): Блокировка до обнуления счетчика.
  • sync.Mutex (Мьютекс):
    • Назначение: Взаимное исключение (эксклюзивный доступ к ресурсу).
    • Lock(): Захват замка. Другие горутины будут ждать.
    • Unlock(): Освобождение замка.
    • Порядок получения доступа к разблокированному мьютексу не гарантирован.
  • sync.RWMutex:
    • Назначение: Оптимизация для частых чтений и редких записей.
    • RLock() / RUnlock(): Позволяют множеству читателей работать одновременно.
    • Lock() / Unlock(): Эксклюзивная блокировка для записи (блокирует и читателей, и писателей).
  • sync.Once: Гарантирует выполнение участка кода ровно один раз.
  • sync/atomic: Низкоуровневые атомарные операции для простых счетчиков.

4. Каналы (Channels)

Идеология и создание

  • Концепция Go: “Не общайтесь через разделяемую память; вместо этого, разделяйте память через общение” (Don’t communicate by sharing memory; share memory by communicating).
  • Определение: Типизированный конвейер для безопасной передачи данных между горутинами.
  • Создание: ch := make(chan int).

Основные операции

Операции записи и чтения по умолчанию являются блокирующими:

  • Запись: ch <- value. Горутина ждет, пока кто-то не прочитает данные.
  • Чтение: val := <-ch. Горутина ждет, пока кто-то не отправит данные.
  • Синхронизация: Момент передачи данных — это “точка встречи” горутин.

Закрытие канала

  • Команда: close(ch).
  • Правило: Канал должен закрывать только отправитель.
  • Последствия закрытия:
    • Чтение из закрытого канала не блокируется и возвращает нулевое значение типа.
    • Проверка состояния: val, ok := <-ch (ok == false означает, что канал закрыт).
    • Запись в закрытый канал вызывает панику.
    • Закрытие канала работает как Broadcast-сигнал: все слушатели разблокируются и узнают о закрытии.

Паттерны: range и select

  • for range: Итерация по каналу до его закрытия.
    for item := range ch { ... }
    
  • select: Ожидание событий сразу из нескольких каналов.
    • Блокируется, пока один из case не станет доступен.
    • Если готовы несколько каналов — выбирает один случайным образом.
    • Используется для реализации Graceful Shutdown (через канал done).

5. Итоги и перспективы

  • Горутина — это высокоуровневая абстракция над системным потоком.
  • Каналы — основной идиоматичный способ обмена данными, исключающий необходимость явных блокировок памяти.
  • Закрытие канала — единственный способ послать сигнал всем получателям одновременно (broadcast).