19 вопросов
Конкурентность - возможность выполнять несколько задач в перекрывающиеся интервалы времени; порядок выполнения может быть чередующимся. Параллелизм - одновременное выполнение на разных ядрах. Конкурентная программа может выполняться на одном ядре (переключение горутин); параллельная - на нескольких.
Взаимная блокировка: две и более горутин ждут друг друга. Например, A держит Lock1 и ждёт Lock2, B держит Lock2 и ждёт Lock1. Программа не продвигается. В Go детектор может сообщить о deadlock при выходе (если все горутины заблокированы).
Горутины не заблокированы, но не продвигаются - постоянно "отступают" или меняют состояние без результата. Пример: два потока при столкновении оба отступают и снова идут - бесконечный цикл отступлений. Реже встречается, чем deadlock.
Горутина долго не получает доступ к ресурсу или CPU из-за других горутин. Например, высокоприоритетные или часто захватывающие мьютекс не дают другим выполниться. В Go планировщик и режим голодания Mutex снижают риск, но не исключают.
Одновременный доступ к одной переменной из разных горутин, причём хотя бы один доступ - запись, без синхронизации. Поведение не определено. Обнаруживается тестами с флагом -race. Исправление - синхронизация (каналы, мьютекс, atomic) или отсутствие разделяемого изменяемого состояния.
При сборке с -race компилятор и рантайм инструментируют доступ к памяти. Во время выполнения отслеживают happens-before и обнаруживают конфликтующие доступы. Выводят отчёт о гонке при обнаружении. Существенные накладные расходы по памяти и скорости - только для тестов и отладки.
go test -race
go run -race ./cmdЕдиный порядок захвата блокировок (всегда сначала Lock1, потом Lock2). Избегать удержания нескольких блокировок; брать только нужную. Использовать таймауты (TryLock, select с time.After). По возможности - каналы и один владелец данных вместо нескольких мьютексов.
Структуры, которые не используют блокировки (мьютексы), а полагаются на атомарные операции (CAS, LL/SC) и повторные попытки. Могут дать лучшую масштабируемость при высокой конкуренции. Сложнее в реализации и отладке. В Go - через sync/atomic или пакеты вроде sync.Map.
Communicating Sequential Processes: процессы (горутины) общаются только через передачу сообщений (каналы), без разделяемой памяти. "Don't communicate by sharing memory; share memory by communicating." В Go каналы - первый способ координации; мьютексы - для случаев, когда разделяемая память нужна.
В lock-free структурах: значение A меняется на B и снова на A. Поток, делающий CAS "если всё ещё A", считает что ничего не изменилось, хотя между ними могли быть другие изменения. Решение - версионирование или tag. В Go реже встречается при использовании atomic.Value и аккуратном проектировании.
Атомарная операция: если текущее значение равно ожидаемому, записать новое и вернуть true; иначе не менять и вернуть false. В Go: atomic.CompareAndSwapInt32, CompareAndSwapInt64 и т.д. Основа для lock-free алгоритмов и спинлоков.
for !atomic.CompareAndSwapInt64(&n, old, new) { old = atomic.LoadInt64(&n) }Встроенная map не защищена от конкурентной записи и одновременной записи и чтения. Одновременная запись и чтение или две записи - data race и возможный крах. Решение: sync.Mutex при доступе к map или sync.Map для специфичных паттернов доступа.
var mu sync.Mutex
mu.Lock(); m[k] = v; mu.Unlock()Каналы - передача владения данными, явные точки синхронизации, модель CSP. Разделяемые переменные с мьютексом - общая память с блокировкой. Каналы подходят для pipeline и передачи данных; мьютекс - для защиты общего состояния. Часто каналы проще рассуждать.
Отношение порядка между событиями в программе. Если A happens-before B, то A видно для B. Синхронизация (канал, мьютекс, atomic) создаёт happens-before между горутинами. Memory model Go определяет, какие наблюдения допустимы при заданной синхронизации.
Порядок видимости операций с памятью между ядрами. Компилятор и процессор могут переупорядочивать операции; барьеры и атомарные операции ограничивают переупорядочивание. В Go sync/atomic даёт нужные гарантии; обычные чтения/записи не гарантируют порядок между горутинами без синхронизации.
Несколько горутин обращаются к разным переменным, но они попадают в одну кеш-линию. Обновление одной переменной инвалидирует кеш-линию у других ядер - лишние промахи кеша и падение производительности. Решение: выравнивание и паддинг, чтобы горячие данные разных горутин были в разных кеш-линиях.
Через atomic: atomic.AddInt64, LoadInt64, StoreInt64. Либо мьютекс и обычная переменная. atomic быстрее при высокой конкуренции для одной переменной.
var counter int64
atomic.AddInt64(&counter, 1)
n := atomic.LoadInt64(&counter)Data race - неопределённое поведение из-за одновременного доступа к памяти без синхронизации (баг). Race condition - логическая ошибка: результат зависит от порядка выполнения горутин, даже при корректной синхронизации (например, проверка и затем действие без атомарности). Оба - источники сбоев.
Да. Если при завершении программы все горутины заблокированы (ждут канал или блокировку), рантайм может вывести "fatal error: all goroutines are asleep - deadlock!". Не все deadlock'и обнаруживаются (например, если есть горутины в select с несколькими case).