Мьютексы

Что такое мьютекс в Go, их виды?

Мьютекс (mutex) в языке программирования Go - это объект, который обеспечивает взаимное исключение доступа к общим ресурсам в многопоточной среде. Он позволяет синхронизировать доступ к критическим секциям кода, предотвращая конфликты и обеспечивая целостность данных при параллельном выполнении кода в горутинах.В Go существуют различные виды мьютексов, включая:

  1. Локальные мьютексы: Существуют только в пределах процесса и могут использоваться любым потоком в этом процессе, имеющим ссылку на локальный объект мьютекса.

  2. Именованные системные мьютексы: Могут быть доступны во всей операционной системе и используются для синхронизации действий между процессами.

  3. Рекурсивные мьютексы: Позволяют потоку захватывать мьютекс несколько раз, что полезно в ситуациях, когда поток может потребовать доступа к ресурсу несколько раз.

Мьютексы в 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

В Go мьютекс представлен структурой sync.Mutex, которая выглядит следующим образом:

type Mutex struct {
    state int32
    sema  uint32
}
  • state — это поле, которое хранит текущее состояние мьютекса.

  • sema — это семафор, используемый для блокировки и разблокировки горутин.

Основные методы 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:

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 (мьютекс для чтения/записи) — это более сложный механизм блокировки, который позволяет различать блокировки для чтения и записи. Он предоставляет два типа блокировок:

  • 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)
}

Основные различия

  1. Типы блокировок:

    • sync.Mutex предоставляет только одну блокировку, которая блокирует доступ как для чтения, так и для записи.

    • sync.RWMutex предоставляет две блокировки: одну для чтения (RLock) и одну для записи (Lock).

  2. Параллелизм:

    • sync.Mutex позволяет только одной горутине владеть мьютексом в любой момент времени.

    • sync.RWMutex позволяет нескольким горутинам одновременно читать ресурс, но только одной горутине изменять его.

  3. Производительность:

    • sync.Mutex может быть более эффективным, если доступ к ресурсу в основном осуществляется для записи.

    • sync.RWMutex может улучшить производительность в сценариях, где чтение происходит гораздо чаще, чем запись, так как позволяет параллельное чтение.

Когда использовать

  • Используйте sync.Mutex, если у вас простой сценарий, где требуется только эксклюзивный доступ к ресурсу.

  • Используйте sync.RWMutex, если у вас есть сценарий с частыми операциями чтения и редкими операциями записи, чтобы улучшить производительность за счет параллельного чтения.

Понимание различий между sync.Mutex и sync.RWMutex поможет вам выбрать правильный механизм блокировки для вашего конкретного сценария и улучшить производительность и безопасность вашей многопоточной программы.

Какие гарантии предоставляет нам RWMutex?

RWMutex (Read-Write Mutex) в Go предоставляет механизмы синхронизации, которые позволяют различать блокировки для чтения и записи. Это позволяет нескольким горутинам одновременно читать данные, но только одной горутине записывать данные. RWMutex реализован в пакете sync.

Гарантии, предоставляемые RWMutex

  1. Множественные читатели:

    • RWMutex позволяет нескольким горутинам одновременно захватывать блокировку для чтения (RLock). Это означает, что несколько горутин могут читать данные одновременно, не блокируя друг друга.

  2. Единственный писатель:

    • RWMutex позволяет только одной горутине захватывать блокировку для записи (Lock). Это означает, что только одна горутина может изменять данные в любой момент времени.

  3. Приоритет записи:

    • Если горутина пытается захватить блокировку для записи (Lock), новые запросы на чтение (RLock) будут блокироваться до тех пор, пока блокировка для записи не будет освобождена. Это предотвращает ситуацию, когда писатель может быть заблокирован бесконечно из-за постоянных запросов на чтение.

  4. Блокировка для записи блокирует чтение:

    • Когда блокировка для записи (Lock) захвачена, все запросы на чтение (RLock) будут блокироваться до тех пор, пока блокировка для записи не будет освобождена.

Пример использования 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()
}

Объяснение кода

  1. Структура SafeCounter:

    • Содержит поле mu типа sync.RWMutex для синхронизации доступа к полю value.

  2. Метод Read:

    • Захватывает блокировку для чтения (RLock) перед чтением значения и освобождает ее после чтения (RUnlock).

  3. Метод Write:

    • Захватывает блокировку для записи (Lock) перед изменением значения и освобождает ее после изменения (Unlock).

  4. Основная функция:

    • Запускает несколько горутин для чтения значения и одну горутину для записи значения, используя RWMutex для синхронизации доступа.

Заключение

RWMutex предоставляет гибкий механизм синхронизации, который позволяет нескольким горутинам одновременно читать данные, но только одной горутине записывать данные. Это может значительно улучшить производительность в сценариях, где чтение данных происходит гораздо чаще, чем запись. Гарантии, предоставляемые RWMutex, включают множественные читатели, единственного писателя, приоритет записи и блокировку чтения при записи.

Last updated