sync.Mutex и sync.RWMutex
Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy. Урок 63 из 107.
Хотя каналы являются предпочтительным способом координации горутин в Go, иногда необходимо напрямую защищать общие данные. Пакет sync предоставляет мьютексы: блокировки, которые гарантируют, что только одна горутина за раз получает доступ к ресурсу.
У sync.Mutex есть два метода: Lock() и Unlock(). Когда горутина вызывает Lock(), она получает эксклюзивный доступ. Другие горутины, вызывающие Lock(), будут заблокированы, пока не будет вызван Unlock():
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Increment() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}Использование defer c.mu.Unlock() гарантирует, что блокировка будет снята, даже если функция завершится досрочно или вызовет панику: это распространённый и рекомендуемый шаблон.
Когда операции чтения выполняются часто, а записи — редко, sync.RWMutex обеспечивает более высокую производительность. Он позволяет нескольким читателям работать одновременно, но писатели получают исключительный доступ:
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock() // разрешено несколько читателей
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // эксклюзивный доступ для записи
defer c.mu.Unlock()
c.data[key] = value
}Используйте RLock()/RUnlock() для операций чтения и Lock()/Unlock() для записи. Это позволяет выполнять чтение одновременно, обеспечивая безопасность записи.
Задание
ЛегкоДавайте создадим потокобезопасную систему управления запасами, которая отслеживает уровни запасов товаров. Ваша система будет безопасно обрабатывать параллельные операции чтения и записи с помощью мьютексов, обеспечивая целостность data, когда multiple operations выполняются одновременно.
Вы организуете код в двух файлах:
inventory.go: Определите потокобезопасную систему управления запасами.Создайте структуру
Inventory, которая хранит количества товаров в map и используетsync.RWMutexдля защиты access. Ваша система inventory должна поддерживать следующие operations:NewInventory() *Inventory— Создаёт новый inventory с инициализированной mapAddStock(product string, quantity int)— Добавляет quantity к запасу товара (используйте exclusive lock, поскольку это modifies data)GetStock(product string) int— Возвращает current запас товара или 0, если он не был found (используйте блокировку чтения, поскольку этот метод only читает data)RemoveStock(product string, quantity int) bool— Удаляет quantity из запаса, если существует достаточный запас. Возвращаетtrueпри успешном выполнении иfalseпри insufficient запасе (используйте exclusive lock)
Не забудьте использовать
deferдля снятия блокировки, чтобы гарантировать правильное освобождение блокировок.main.go: Считывайте operations и демонстрируйте работу вашей потокобезопасной системы inventory.Считайте number operations, затем обработайте каждую operation. Каждая operation содержит тип (
add,getилиremove), имя товара и для operationsadd/remove— quantity.Для каждой operation выведите результат:
add: ВыведитеAdded [quantity] [product]get: Выведите[product]: [stock] in stockremove: При успешном выполнении выведитеRemoved [quantity] [product], а в противном случае —Insufficient stock for [product]
Будут предоставлены следующие входные data:
- Строка 1: Number operations (целое число)
- Следующие строки: Для каждой operation:
- Тип operation (
add,getилиremove) - Имя товара
- Quantity (only для operations
addиremove)
- Тип operation (
Например, если задано:
5
add
apples
50
get
apples
remove
apples
30
remove
apples
25
get
applesВаш вывод должен быть следующим:
Added 50 apples
apples: 50 in stock
Removed 30 apples
Insufficient stock for apples
apples: 20 in stockRWMutex позволяет multiple вызовам GetStock одновременно выполнять чтение, тогда как AddStock и RemoveStock получают exclusive access при изменении inventory.
Попробуйте сами
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Прочитать количество операций
line, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(line))
// Создать новый инвентарь
inventory := NewInventory()
// Обработать каждую операцию
for i := 0; i < numOps; i++ {
// Прочитать тип операции
opLine, _ := reader.ReadString('\n')
opType := strings.TrimSpace(opLine)
// Прочитать название продукта
productLine, _ := reader.ReadString('\n')
product := strings.TrimSpace(productLine)
// TODO: Обработать каждый тип операции (add, get, remove)
// Для "add" и "remove" прочитать количество из ввода
// Вызвать соответствующий метод inventory
// Вывести результат согласно описанию задания
switch opType {
case "add":
// TODO: Прочитать количество, добавить запас, вывести результат
case "get":
// TODO: Получить запас, вывести результат
case "remove":
// TODO: Прочитать количество, удалить запас, вывести соответствующий результат
}
}
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП в Go
Внешние файлыРабочее пространство и модули GoПакеты и импортыЭкспортируемые и неэкспортируемые именаВведение в ООП в GoСтруктуры как классыОпределение методов структурПолучатели-указатели и получатели-значенияИнициализация структурФункции-конструкторыИтоги — Простой калькулятор4Интерфейсы
Введение в интерфейсыНеявная реализацияИнтерфейс как контрактПустой интерфейс (any)Утверждение типаПереключатель типовКомпозиция интерфейсовИнтерфейсы Stringer и ErrorПовторение: Калькулятор фигур7Инкапсуляция
Экспортируемые и неэкспортируемые поляИнкапсуляция на уровне пакетовГеттеры и сеттерыСокрытие информации в GoИтоги — Записи о студентах10Обобщения (Generics) (Go 1.18+)
Введение в GenericsПараметры типовОграничения типовОбобщенные структурыОбходной путь для обобщенных методовИтоги — Обобщенная коллекция13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн Abstract FactoryПаттерн ObserverПаттерн Strategy2Глубокое погружение в типы и структуры
Базовые и составные типыОпределение пользовательских типовТеги структурАнонимные структурыВложенные структурыНулевые значения и значения по умолчаниюПовторение — Контактная книга5Композиция вместо наследования
Почему в Go нет наследованияОсновы встраивания структурПродвижение методовВстраивание нескольких структурВстраивание против агрегацииЗатенение встроенных методовИтоги — Иерархия сотрудников8Обработка ошибок и ООП
Интерфейс errorПользовательские типы ошибокОбертывание ошибок (fmt.Errorf)Sentinel-ошибкиerrors.Is() и errors.As()Panic, Defer и RecoverИтоги — Парсер файлов11Стандартная библиотека и ООП
io.Reader и io.Writersort.InterfaceИнтерфейс fmt.Stringerencoding/json со структурамиИнтерфейс http.HandlerПовторение: модели REST API14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикMiddleware как Декоратор3Указатели и память
Основы указателей в GoУказатели на структурыПередача по значению и по ссылкеФункция new()Сборка мусора в GoПовторение: Конструктор связного списка6Полиморфизм в Go
Полиморфизм через интерфейсыУтиная типизация в GoПравила реализации интерфейсовПолиморфные коллекцииВнедрение зависимостейИтоги — Обработчик платежей9Конкурентность и ООП
Основы горутинКаналы и взаимодействиеБуферизованные и небуферизованные каналыОператор selectsync.Mutex и sync.RWMutexsync.WaitGroupПроектирование потокобезопасных структурПовторение — Worker PoolПотренируйтесь самостоятельно: Онлайн-компилятор Go