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() для операций записи. Это позволяет выполнять чтение одновременно, обеспечивая безопасность записи.
Задание
ЛегкоДавайте создадим потокобезопасную систему управления запасами, которая отслеживает уровни запасов товаров. Ваша система будет безопасно обрабатывать параллельные операции чтения и записи с помощью мьютексов, обеспечивая целостность данных при одновременном выполнении нескольких операций.
Вы организуете код в двух файлах:
inventory.go: Определите потокобезопасную систему управления запасами.Создайте структуру
Inventory, которая хранит количества товаров в map и используетsync.RWMutexдля защиты доступа. Ваша система управления запасами должна поддерживать следующие операции:NewInventory() *Inventory— Создаёт новую систему управления запасами с инициализированной mapAddStock(product string, quantity int)— Добавляет количество к запасу товара (используйте эксклюзивную блокировку, поскольку эта операция изменяет данные)GetStock(product string) int— Возвращает текущий запас товара или 0, если товар не найден (используйте блокировку чтения, поскольку эта операция только читает данные)RemoveStock(product string, quantity int) bool— Удаляет количество из запаса, если доступно достаточное количество товара. Возвращаетtrueв случае успеха иfalse, если запаса недостаточно (используйте эксклюзивную блокировку)
Не забудьте использовать
deferдля снятия блокировки, чтобы гарантировать её корректное освобождение.main.go: Считайте операции и продемонстрируйте работу потокобезопасной системы управления запасами.Считайте количество операций, а затем обработайте каждую операцию. Каждая операция содержит тип (
add,getилиremove), название товара и, для операцийadd/remove, количество.Для каждой операции выведите результат:
add: ВыведитеAdded [quantity] [product]get: Выведите[product]: [stock] in stockremove: В случае успеха выведитеRemoved [quantity] [product], а в противном случае —Insufficient stock for [product]
Будут предоставлены следующие входные данные:
- Строка 1: Количество операций (целое число)
- Следующие строки: Для каждой операции:
- Тип операции (
add,getилиremove) - Название товара
- Количество (только для операций
addиremove)
- Тип операции (
Например, если даны следующие данные:
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 позволяет нескольким вызовам GetStock одновременно выполнять чтение, тогда как AddStock и RemoveStock получают эксклюзивный доступ при изменении запасов.
Попробуйте сами
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