Практическая ситуация: несколько горутин обновляют общий счётчик, а передавать каждое обновление отдельному потребителю не требуется. Какой примитив синхронизации выбрать?
Выберите mutex, обычно sync.Mutex: он напрямую защищает общий счётчик и не требует отдельной горутины-потребителя. Канал здесь добавил бы посредника, управление жизненным циклом и возможную блокировку из-за заполнения буфера без дополнительной пользы.
Если операция ограничивается простым инкрементом числового значения, альтернативой может быть атомарная операция. Но среди каналов и mutex для общего изменяемого состояния предпочтителен mutex.
Взаимное исключение появилось как прямой способ безопасного доступа нескольких потоков или горутин к общей памяти. Оно решает задачу, когда состояние должно оставаться общим, а операции над ним должны выполняться последовательно.
Каналы в Go предназначены прежде всего для передачи данных и координации между горутинами. Их можно использовать для сериализации обновлений, но это меняет архитектуру: появляется владелец состояния, которому нужно доставлять сообщения и управлять его завершением.
Обычное увеличение счётчика состоит из чтения текущего значения, вычисления нового и записи результата. Если несколько горутин выполняют эти действия одновременно без синхронизации, они могут прочитать одно и то же старое значение, поэтому часть увеличений потеряется.
Такое поведение является состоянием гонки. Результат становится зависимым от планирования горутин, а проверка программы на малом числе итераций может случайно не обнаружить ошибку.
Использование канала ради одного счётчика тоже имеет последствия: нужно запускать горутину, принимающую обновления, определить момент её завершения и решить, что делать при переполнении буфера. При прямом доступе к общему состоянию эти дополнительные элементы не нужны.
Mutex предоставляет две ключевые операции: Lock захватывает право доступа, а Unlock освобождает его. Пока одна горутина удерживает mutex, другие горутины, пытающиеся захватить тот же mutex, ждут; поэтому критическая секция выполняется без одновременного изменения защищаемого состояния.
Минимальный пример:
Mutex должен защищать все обращения к данным, а не только запись. Если чтение выполняется без блокировки, оно тоже может участвовать в гонке. Освобождение mutex следует гарантировать даже при раннем выходе, обычно размещая Unlock через defer сразу после успешного Lock.
Критическую секцию держат как можно короче. Нельзя вызывать внутри неё неизвестный или потенциально блокирующий код без необходимости: это увеличивает конкуренцию и может привести к взаимной блокировке, если вызываемый код попытается захватить тот же ресурс.
Для одного числового счётчика часто подходит пакет sync/atomic: атомарный инкремент дешевле и не требует mutex. Однако при изменении нескольких полей, проверке условий или поддержании инварианта mutex обычно понятнее, потому что объединяет связанные операции в одну защищённую секцию.
Канал уместнее, когда обновление является сообщением в конвейере, порядок сообщений важен или состояние намеренно принадлежит одной горутине. Mutex предпочтительнее, когда горутины должны синхронно работать с общей структурой данных без отдельного обработчика.
В обработчике запросов нужно обновлять число успешных операций и время последнего успеха. Рассматривались три варианта: передавать события через канал агрегатору, защищать два поля mutex и применять атомарные операции.
Канал позволил бы полностью изолировать состояние в одной горутине, но потребовал бы обработки очереди, завершения агрегатора и решения вопроса о переполнении. Атомарные операции подходят для независимых чисел, но неудобны, если нужно согласованно обновить оба поля и сохранить связь между ними.
Выбран mutex, поскольку оба значения образуют единое состояние и должны изменяться согласованно. Критическая секция заняла только обновление полей, поэтому обработка запросов не блокировалась на сетевых или файловых операциях. В результате решение сохранило простую модель владения данными и не добавило отдельный жизненный цикл для агрегирующей горутины.
Нет. Конкурентное чтение также должно быть синхронизировано, если параллельно выполняется запись. Иначе чтение может участвовать в состоянии гонки, даже если все записи защищены mutex.
Варианты — читать значение под тем же mutex, использовать sync.RWMutex при действительно большом числе независимых читателей или применять атомарное чтение для атомарно представимого значения. Выбор должен соответствовать способу записи: нельзя смешивать несогласованные методы без доказательства их безопасности.
sync.Mutex после начала его использования?Копировать используемый mutex нельзя: копия не становится продолжением того же протокола взаимного исключения. Разные копии могут разрешить одновременный доступ к одной и той же данным, а внутреннее состояние заблокированного mutex может быть некорректно перенесено.
Обычно mutex размещают внутри структуры и передают такую структуру по указателю. Методы, работающие с защищаемыми полями, также часто имеют указательный получатель, чтобы не копировать структуру вместе с mutex.
Нет. Он защищает от одновременного выполнения критической секции, но не предотвращает неправильный порядок захвата нескольких mutex. Например, одна горутина может удерживать первый mutex и ждать второй, пока другая удерживает второй и ждёт первый.
Чтобы снизить риск, устанавливают единый порядок захвата ресурсов, не удерживают mutex дольше необходимого и не вызывают под ним код с неизвестным поведением. Инструменты диагностики, включая go test -race, помогают находить гонки, но логическую взаимную блокировку нужно предотвращать проектированием.