Допустимо ли копировать sync.Mutex после его первого использования?
Нет. После первого использования sync.Mutex копировать нельзя: копия содержит отдельное внутреннее состояние блокировки, поэтому разные копии могут ошибочно считаться защитой одного и того же общего ресурса. Это приводит к гонкам данных, зависаниям или разблокировке не того экземпляра.
Мьютекс нужен для взаимного исключения: в каждый момент времени только одна горутина должна выполнять критическую секцию. В Go sync.Mutex имеет нулевое состояние, поэтому его можно объявить без конструктора, но это состояние относится к конкретному экземпляру мьютекса.
Проблема копирования возникает из общего правила для примитивов синхронизации: после начала использования они должны сохранять идентичность. Копирование значения создает другой объект, а не вторую ссылку на тот же мьютекс.
Если структура с мьютексом передается в функцию по значению или метод объявлен с приемником-значением, копируется вся структура вместе с мьютексом. Код может выглядеть синхронизированным, хотя разные горутины блокируют разные экземпляры и одновременно обращаются к одним данным.
Особенно опасно копирование мьютекса в момент, когда он заблокирован: копия может сохранить состояние заблокированного мьютекса, но не иметь владельца, который сможет корректно его разблокировать. В результате вызов блокировки на копии способен зависнуть навсегда.
Правильный подход — не копировать sync.Mutex после начала использования и обычно передавать содержащую его структуру по указателю. Методы такой структуры следует объявлять с указательным приемником, чтобы они работали с одним и тем же мьютексом.
Здесь все вызовы Inc обращаются к одному полю mu, поэтому блокировка действительно защищает поле n. Передача Counter по значению после начала использования была бы ошибочной, даже если значение n само по себе копируется ожидаемым образом.
Нельзя заменять мьютекс копированием структуры, содержащей его, в надежде получить независимые защищенные объекты: это допустимо только если объекты действительно полностью независимы и мьютекс еще не использовался. На практике безопаснее проектировать такие типы как некопируемые после создания.
Инструмент go vet умеет выявлять многие случаи копирования типов, содержащих примитивы синхронизации, но он не заменяет понимание владения и способов передачи значений. Указатель устраняет копирование самой структуры, однако копирование указателя, конечно, создает лишь еще одну ссылку на тот же объект.
Предположим, счетчик хранится в структуре, а метод увеличения объявлен с приемником-значением. Каждый вызов метода получает копию структуры и блокирует собственную копию sync.Mutex; исходное поле счетчика при этом тоже изменяется в копии, поэтому результат может оставаться неизменным, а защита исходного состояния фактически отсутствует.
Рассматривались три варианта:
Выбран третий вариант. Структура создается один раз, методы получают указатель, а доступ к общему состоянию выполняется под одним мьютексом. В результате исчезает ложная синхронизация копий и сохраняется локальная, понятная критическая секция.
Формально запрет относится к копированию после первого использования. Нулевое состояние sync.Mutex можно скопировать до начала работы с ним, но такой дизайн легко становится хрупким: если один экземпляр уже начал использоваться, последующее неявное копирование превращается в ошибку. Поэтому типы с мьютексом обычно не копируют вообще и передают по указателю.
Указательный приемник гарантирует, что метод работает с исходным мьютексом и исходными полями структуры, а не с их копиями. Однако он не делает автоматически безопасными обращения к полям вне этого метода: все конкурентные пути доступа к общему состоянию по-прежнему должны соблюдать единый протокол синхронизации.
Копия может получить внутреннее состояние, соответствующее заблокированному мьютексу, но не получает корректной связи с горутиной, которая владеет исходным экземпляром. Попытка заблокировать такую копию может ожидать разблокировки, которая никогда не произойдет; даже если конкретная реализация или сценарий не приводит к немедленному зависанию, поведение не является допустимым способом синхронизации. Мьютекс нельзя копировать независимо от того, находится ли он в этот момент заблокированным или разблокированным состоянии.