cpp

Синхронизация потоков с помощью мьютексов

Если несколько потоков пытаются получить доступ к одному ресурсу, то результат этого действия может стать непредсказуемым. Чтобы не допустить проблем и избежать состояния гонок необходимо выполнять синхронизацию критичных секций (секций, к которым имеют доступ несколько потоков). При доступе к синхронизированной секции поток запрашивает блокировку. Если блокировка получена, то поток изменяет что-то внутри синхронизированного блока. Если не удалось получить блокировку, то поток блокируется до момента получения разрешения. Таким образом, код внутри критичной секции в один момент времени выполняется только одним потоком как атомарная операция.

Рассмотрим проблему на примере (листинг 18.4).

Листинг 18.4. Проблема при отсутствии синхронизации

package main

import (
   "fmt"
   "sync"
   "time"
)

func main() {
   var wg sync.WaitGroup
   count := 0
   for i := 1; i < 4; i++ {
      wg.Add(1)
      go func() {
         defer wg.Done()
         for j := 1; j < 11; j++ {
            n := count
            time.Sleep(10 * time.Microsecond)
            count = n + 1
         }
      }()
   }
   wg.Wait()
   fmt.Println("count =", count)
}

В этом примере несколько потоков пытаются изменить значение переменной count без синхронизации. Причем специально операцию выполняем с помощью нескольких инструкций. В любой момент времени операция может быть прервана на любой инструкции. Поэтому вместо значения 30 переменная count получит любое произвольное значение, например, 10.

Выполнить синхронизацию критичной секции позволяет структура Mutex из пакета sync. Структура содержит следующие методы:

  • Lock() — устанавливает блокировку. Если блокировка уже используется, то поток будет ожидать снятия блокировки. Формат метода:
(*sync.Mutex).Lock()
  • Unlock() — снимает блокировку. Блокировка обязательно должна быть снята после выхода из критичной секции, иначе все остальные потоки будут заблокированы навсегда. Формат метода:
(*sync.Mutex).Unlock()
Обратите внимание

Пример использования структуры Mutex приведен в листинге 18.5.

Листинг 18.5. Структура Mutex

package main

import (
   "fmt"
   "sync"
   "time"
)

func main() {
   var wg sync.WaitGroup
   var mutex sync.Mutex
   count := 0
   for i := 1; i < 4; i++ {
      wg.Add(1)
      go func() {
         defer wg.Done()
         for j := 1; j < 11; j++ {
            mutex.Lock()   // Устанавливаем блокировку
            n := count
            time.Sleep(10 * time.Microsecond)
            count = n + 1
            mutex.Unlock() // Снимаем блокировку
         }
      }()
   }
   wg.Wait()
   fmt.Println("count =", count)
}

Теперь при использовании мьютекса проблем не будет и значение переменной count всегда будет равно 30, а не произвольному числу.

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

Структура RWMutex содержит следующие методы:

  • Lock() — устанавливает блокировку для записи. Если блокировка на чтение или запись уже используется, то поток будет ожидать снятия блокировки. Формат метода:
(*sync.RWMutex).Lock()
  • Unlock() — снимает блокировку для записи. Блокировка обязательно должна быть снята после выхода из критичной секции, иначе все остальные потоки будут заблокированы навсегда. Формат метода:
(*sync.RWMutex).Unlock()
  • RLock() — устанавливает блокировку для чтения. Если блокировка на запись уже используется, то поток будет ожидать снятия блокировки. Формат метода:
(*sync.RWMutex).RLock()
  • RUnlock() — снимает блокировку для чтения. Блокировка обязательно должна быть снята после выхода из критичной секции, иначе все остальные потоки будут заблокированы навсегда. Формат метода:
(*sync.RWMutex).RUnlock()
  • RLocker() — возвращает объект интерфейса Locker, который содержит методы для установки и снятия блокировки для чтения. Формат метода:
(*sync.RWMutex).RLocker() sync.Locker
type Locker interface {
   Lock()
   Unlock()
}
Обратите внимание

Пример использования структуры RWMutex приведен в листинге 18.6.

Листинг 18.6. Структура RWMutex

package main

import (
   "fmt"
   "sync"
   "time"
)

func main() {
   var wg sync.WaitGroup
   var mutex sync.RWMutex
   count := 0
   for i := 1; i < 4; i++ {
      wg.Add(1)
      go func() {
         defer wg.Done()
         for j := 1; j < 11; j++ {
            mutex.Lock() // Устанавливаем блокировку для записи
            n := count
            time.Sleep(10 * time.Microsecond)
            count = n + 1
            mutex.Unlock() // Снимаем блокировку для записи
         }
      }()
      wg.Add(1)
      go func(n int) {
         defer wg.Done()
         for j := 1; j < 31; j++ {
            // loc := mutex.RLocker()
            // loc.Lock()
            mutex.RLock() // Устанавливаем блокировку для чтения
            fmt.Println(n, "count =", count)
            // loc.Unlock()
            mutex.RUnlock() // Снимаем блокировку для чтения
            time.Sleep(10 * time.Microsecond)
         }
      }(i)
   }
   wg.Wait()
   fmt.Println("count =", count)
}

Учебник Go (Golang)
Учебник Go (Golang) в формате PDF

Помощь сайту

ЮMoney (Yandex-деньги): 410011140483022

ПАО Сбербанк:
Счет: 40817810855006152256
Реквизиты банка:
Наименование: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК
Корреспондентский счет: 30101810500000000653
БИК: 044030653
КПП: 784243001
ОКПО: 09171401
ОКОНХ: 96130
Скриншот реквизитов

cpp