Мьютексы
Что такое мьютекс в Go, их виды?
Мьютекс (mutex) в языке программирования Go - это объект, который обеспечивает взаимное исключение доступа к общим ресурсам в многопоточной среде. Он позволяет синхронизировать доступ к критическим секциям кода, предотвращая конфликты и обеспечивая целостность данных при параллельном выполнении кода в горутинах.В Go существуют различные виды мьютексов, включая:
Локальные мьютексы: Существуют только в пределах процесса и могут использоваться любым потоком в этом процессе, имеющим ссылку на локальный объект мьютекса.
Именованные системные мьютексы: Могут быть доступны во всей операционной системе и используются для синхронизации действий между процессами.
Рекурсивные мьютексы: Позволяют потоку захватывать мьютекс несколько раз, что полезно в ситуациях, когда поток может потребовать доступа к ресурсу несколько раз.
Мьютексы в Go предоставляют безопасность доступа к общим ресурсам в многопоточной среде, предотвращая race conditions и обеспечивая правильную синхронизацию между горутинами. Они являются важным инструментом для разработки параллельных программ, где необходимо обеспечить правильное взаимодействие между потоками.
Пример:
package main
import (
"fmt"
"sync"
)
func main() {
// Создание общего счетчика
var counter int
// Создание мьютекса
var mutex sync.Mutex
// Создание группы ожидания
var wg sync.WaitGroup
// Добавление 10 горутин в группу ожидания
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// Захват мьютекса
mutex.Lock()
defer mutex.Unlock()
// Увеличение счетчика
counter++
fmt.Printf("Счетчик: %d\n", counter)
}()
}
// Ожидание завершения всех горутин
wg.Wait()
fmt.Println("Программа завершена.")
}
/*
В этом примере:
Создается общий счетчик counter, который будет увеличиваться в горутинах.
Создается мьютекс mutex для синхронизации доступа к счетчику.
Создается группа ожидания wg для ожидания завершения всех горутин.
Запускается 10 горутин, каждая из которых:
Захватывает мьютекс с помощью mutex.Lock().
Увеличивает счетчик и выводит его значение.
Освобождает мьютекс с помощью mutex.Unlock().
Сообщает группе ожидания о завершении работы.
Программа ожидает завершения всех горутин с помощью wg.Wait().
Использование мьютекса в этом примере гарантирует,
что только один поток может одновременно изменять значение счетчика,
предотвращая гонки данных и обеспечивая целостность данных.
*/
Как работают мьютексы под капотом?
Мьютексы (mutexes) в языке программирования Go реализованы с использованием примитивов синхронизации, предоставляемых операционной системой и процессором. Внутренняя реализация мьютексов в Go находится в пакете sync
и использует атомарные операции и системные вызовы для обеспечения корректной работы в многопоточной среде.
Внутренняя структура sync.Mutex
sync.Mutex
В Go мьютекс представлен структурой sync.Mutex
, которая выглядит следующим образом:
type Mutex struct {
state int32
sema uint32
}
state
— это поле, которое хранит текущее состояние мьютекса.sema
— это семафор, используемый для блокировки и разблокировки горутин.
Основные методы sync.Mutex
sync.Mutex
Lock
Метод Lock
используется для захвата мьютекса. Если мьютекс уже захвачен другой горутиной, текущая горутина будет заблокирована до тех пор, пока мьютекс не будет освобожден.
func (m *Mutex) Lock() {
if atomic.CompareAndSwapInt32(&m.state, 0, 1) {
return
}
m.lockSlow()
}
atomic.CompareAndSwapInt32(&m.state, 0, 1)
— это атомарная операция, которая пытается установить состояние мьютекса в1
(захвачено), если текущее состояние равно0
(свободно). Если операция успешна, мьютекс захвачен, и метод возвращается.Если атомарная операция не удалась (мьютекс уже захвачен), вызывается метод
lockSlow
, который обрабатывает более сложные случаи.
lockSlow
Метод lockSlow
используется для обработки случаев, когда мьютекс уже захвачен другой горутиной. Он включает в себя блокировку текущей горутины с использованием семафора.
func (m *Mutex) lockSlow() {
for {
old := m.state
new := old | mutexLocked
if old&mutexLocked != 0 {
new += mutexWaiterShift
}
if atomic.CompareAndSwapInt32(&m.state, old, new) {
if old&mutexLocked == 0 {
break
}
runtime_Semacquire(&m.sema)
}
}
}
Цикл
for
продолжает попытки захвата мьютекса.old := m.state
сохраняет текущее состояние мьютекса.new := old | mutexLocked
устанавливает битmutexLocked
в новом состоянии.Если мьютекс уже захвачен (
old&mutexLocked != 0
), увеличивается счетчик ожидающих горутин (new += mutexWaiterShift
).atomic.CompareAndSwapInt32(&m.state, old, new)
пытается обновить состояние мьютекса.Если мьютекс был свободен (
old&mutexLocked == 0
), цикл завершается.Если мьютекс был захвачен, текущая горутина блокируется с использованием семафора (
runtime_Semacquire(&m.sema)
).
Unlock
Метод Unlock
используется для освобождения мьютекса. Если есть горутины, ожидающие захвата мьютекса, одна из них будет разблокирована.
func (m *Mutex) Unlock() {
new := atomic.AddInt32(&m.state, -1)
if (new+1)&mutexLocked == 0 {
panic("sync: unlock of unlocked mutex")
}
if new&mutexWaiterShift != 0 {
runtime_Semrelease(&m.sema)
}
}
new := atomic.AddInt32(&m.state, -1)
уменьшает состояние мьютекса на1
.Если мьютекс уже был свободен (
(new+1)&mutexLocked == 0
), вызывается паника, так как это означает попытку разблокировки уже разблокированного мьютекса.Если есть ожидающие горутины (
new&mutexWaiterShift != 0
), одна из них разблокируется с использованием семафора (runtime_Semrelease(&m.sema)
).
Атомарные операции и семафоры
Атомарные операции: Go использует пакет
sync/atomic
для выполнения атомарных операций, таких какCompareAndSwapInt32
иAddInt32
. Эти операции гарантируют, что изменения состояния мьютекса будут выполнены атомарно, без гонок данных.Семафоры: Go использует семафоры для блокировки и разблокировки горутин. Функции
runtime_Semacquire
иruntime_Semrelease
вызываются для блокировки и разблокировки горутин соответственно.
Заключение
Мьютексы в Go реализованы с использованием атомарных операций и семафоров для обеспечения корректной работы в многопоточной среде. Они предоставляют простой и эффективный способ управления доступом к общим ресурсам, предотвращая гонки данных и обеспечивая синхронизацию между горутинами.
В чем разница между mutex и RWMutex?
В языке программирования Go sync.Mutex
и sync.RWMutex
являются примитивами синхронизации, которые используются для управления доступом к общим ресурсам в многопоточных программах. Однако они имеют разные цели и предоставляют разные уровни блокировки.
sync.Mutex
sync.Mutex
sync.Mutex
(мьютекс) — это простой механизм блокировки, который позволяет только одной горутине владеть мьютексом в любой момент времени. Он используется для обеспечения эксклюзивного доступа к ресурсу.
Пример использования sync.Mutex
:
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock() // Захватываем мьютекс
counter++ // Изменяем общий ресурс
mu.Unlock() // Освобождаем мьютекс
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Final counter:", counter)
}
sync.RWMutex
sync.RWMutex
sync.RWMutex
(мьютекс для чтения/записи) — это более сложный механизм блокировки, который позволяет различать блокировки для чтения и записи. Он предоставляет два типа блокировок:
RLock (блокировка для чтения): Позволяет нескольким горутинам одновременно читать ресурс.
Lock (блокировка для записи): Позволяет только одной горутине изменять ресурс, блокируя доступ для чтения и других записей.
Пример использования sync.RWMutex
:
package main
import (
"fmt"
"sync"
)
var (
counter int
rwMu sync.RWMutex
)
func readCounter() int {
rwMu.RLock() // Захватываем блокировку для чтения
defer rwMu.RUnlock() // Освобождаем блокировку для чтения
return counter
}
func increment() {
rwMu.Lock() // Захватываем блокировку для записи
counter++ // Изменяем общий ресурс
rwMu.Unlock() // Освобождаем блокировку для записи
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("Counter:", readCounter())
}()
}
wg.Wait()
fmt.Println("Final counter:", counter)
}
Основные различия
Типы блокировок:
sync.Mutex
предоставляет только одну блокировку, которая блокирует доступ как для чтения, так и для записи.sync.RWMutex
предоставляет две блокировки: одну для чтения (RLock) и одну для записи (Lock).
Параллелизм:
sync.Mutex
позволяет только одной горутине владеть мьютексом в любой момент времени.sync.RWMutex
позволяет нескольким горутинам одновременно читать ресурс, но только одной горутине изменять его.
Производительность:
sync.Mutex
может быть более эффективным, если доступ к ресурсу в основном осуществляется для записи.sync.RWMutex
может улучшить производительность в сценариях, где чтение происходит гораздо чаще, чем запись, так как позволяет параллельное чтение.
Когда использовать
Используйте
sync.Mutex
, если у вас простой сценарий, где требуется только эксклюзивный доступ к ресурсу.Используйте
sync.RWMutex
, если у вас есть сценарий с частыми операциями чтения и редкими операциями записи, чтобы улучшить производительность за счет параллельного чтения.
Понимание различий между sync.Mutex
и sync.RWMutex
поможет вам выбрать правильный механизм блокировки для вашего конкретного сценария и улучшить производительность и безопасность вашей многопоточной программы.
Какие гарантии предоставляет нам RWMutex?
RWMutex
(Read-Write Mutex) в Go предоставляет механизмы синхронизации, которые позволяют различать блокировки для чтения и записи. Это позволяет нескольким горутинам одновременно читать данные, но только одной горутине записывать данные. RWMutex
реализован в пакете sync
.
Гарантии, предоставляемые RWMutex
RWMutex
Множественные читатели:
RWMutex
позволяет нескольким горутинам одновременно захватывать блокировку для чтения (RLock
). Это означает, что несколько горутин могут читать данные одновременно, не блокируя друг друга.
Единственный писатель:
RWMutex
позволяет только одной горутине захватывать блокировку для записи (Lock
). Это означает, что только одна горутина может изменять данные в любой момент времени.
Приоритет записи:
Если горутина пытается захватить блокировку для записи (
Lock
), новые запросы на чтение (RLock
) будут блокироваться до тех пор, пока блокировка для записи не будет освобождена. Это предотвращает ситуацию, когда писатель может быть заблокирован бесконечно из-за постоянных запросов на чтение.
Блокировка для записи блокирует чтение:
Когда блокировка для записи (
Lock
) захвачена, все запросы на чтение (RLock
) будут блокироваться до тех пор, пока блокировка для записи не будет освобождена.
Пример использования RWMutex
RWMutex
Рассмотрим пример, где несколько горутин читают данные, и одна горутина записывает данные, используя RWMutex
для синхронизации:
package main
import (
"fmt"
"sync"
"time"
)
type SafeCounter struct {
mu sync.RWMutex
value int
}
func (c *SafeCounter) Read() int {
c.mu.RLock()
defer c.mu.RUnlock()
return c.value
}
func (c *SafeCounter) Write(val int) {
c.mu.Lock()
defer c.mu.Unlock()
c.value = val
}
func main() {
counter := SafeCounter{}
var wg sync.WaitGroup
// Запуск нескольких горутин для чтения
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 5; j++ {
fmt.Printf("Reader %d: %d\n", id, counter.Read())
time.Sleep(100 * time.Millisecond)
}
}(i)
}
// Запуск одной горутины для записи
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 5; i++ {
counter.Write(i)
fmt.Printf("Writer: %d\n", i)
time.Sleep(150 * time.Millisecond)
}
}()
wg.Wait()
}
Объяснение кода
Структура
SafeCounter
:Содержит поле
mu
типаsync.RWMutex
для синхронизации доступа к полюvalue
.
Метод
Read
:Захватывает блокировку для чтения (
RLock
) перед чтением значения и освобождает ее после чтения (RUnlock
).
Метод
Write
:Захватывает блокировку для записи (
Lock
) перед изменением значения и освобождает ее после изменения (Unlock
).
Основная функция:
Запускает несколько горутин для чтения значения и одну горутину для записи значения, используя
RWMutex
для синхронизации доступа.
Заключение
RWMutex
предоставляет гибкий механизм синхронизации, который позволяет нескольким горутинам одновременно читать данные, но только одной горутине записывать данные. Это может значительно улучшить производительность в сценариях, где чтение данных происходит гораздо чаще, чем запись. Гарантии, предоставляемые RWMutex
, включают множественные читатели, единственного писателя, приоритет записи и блокировку чтения при записи.
Last updated