Оглавление
- Основы конкурентности
- Горутины (Goroutines)
- Состояние гонки и синхронизация (Пакет sync)
- Каналы (Channels)
- Итоги и перспективы
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).