Menu
Coddy logo textTech

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() для записи. Это позволяет выполнять параллельное чтение, обеспечивая при этом безопасность записи.

challenge icon

Задание

Легко

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

Вы организуете свой код в двух файлах:

  • inventory.go: Определите вашу потокобезопасную систему управления инвентарем.

    Создайте структуру Inventory, которая хранит количество товаров в карте (map) и использует sync.RWMutex для защиты доступа. Ваш инвентарь должен поддерживать следующие операции:

    • NewInventory() *Inventory — создает новый инвентарь с инициализированной картой
    • AddStock(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 stock
    • remove: Выведите 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 stock

RWMutex позволяет нескольким вызовам GetStock выполнять чтение одновременно, в то время как AddStock и RemoveStock получают эксклюзивный доступ при изменении инвентаря.

Шпаргалка

Пакет sync предоставляет мьютексы для защиты общих данных, когда нескольким горутинам требуется прямой доступ к одному и тому же ресурсу.

sync.Mutex

sync.Mutex обеспечивает эксклюзивный доступ с помощью 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 с Unlock() гарантирует, что блокировка будет снята, даже если функция завершится досрочно или возникнет паника.

sync.RWMutex

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() для операций записи.

Попробуйте сами

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" прочитать количество из ввода
		// Вызвать соответствующий метод инвентаря
		// Вывести результат согласно описанию задачи
		
		switch opType {
		case "add":
			// TODO: Прочитать количество, добавить на склад, вывести результат
			
		case "get":
			// TODO: Получить остаток на складе, вывести результат
			
		case "remove":
			// TODO: Прочитать количество, списать со склада, вывести соответствующий результат
			
		}
	}
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование