sync.Mutex & sync.RWMutex
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy. Lektion 63 von 107.
Während Kanäle Gos bevorzugte Methode zur Koordination von Goroutinen sind, musst du manchmal gemeinsam genutzte Daten direkt schützen. Das Paket sync stellt Mutexe bereit: Sperren, die sicherstellen, dass jeweils nur eine Goroutine auf eine Ressource zugreift.
Ein sync.Mutex verfügt über zwei Methoden: Lock() und Unlock(). Wenn eine Goroutine Lock() aufruft, erhält sie exklusiven Zugriff. Andere Goroutinen, die Lock() aufrufen, blockieren, bis Unlock() aufgerufen wird:
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
}Die Verwendung von defer c.mu.Unlock() stellt sicher, dass die Sperre auch dann freigegeben wird, wenn die Funktion vorzeitig zurückkehrt oder eine Panik auslöst: ein gängiges und empfohlenes Muster.
Wenn Lesevorgänge häufig, Schreibvorgänge jedoch selten sind, bietet sync.RWMutex eine bessere Leistung. Es ermöglicht mehreren Lesern den gleichzeitigen Zugriff, aber Schreiber erhalten exklusiven Zugriff:
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock() // mehrere Leser erlaubt
defer c.mu.RUnlock()
return c.data[key]
}
func (c *Cache) Set(key, value string) {
c.mu.Lock() // exklusiver Zugriff zum Schreiben
defer c.mu.Unlock()
c.data[key] = value
}Verwende RLock()/RUnlock() für Leseoperationen und Lock()/Unlock() für Schreibvorgänge. Dadurch werden gleichzeitige Lesevorgänge ermöglicht und gleichzeitig sichere Schreibvorgänge gewährleistet.
Aufgabe
EinfachErstellen wir ein threadsicheres Inventarsystem, das die Lagerbestände von Produkten verfolgt. Dein System verarbeitet gleichzeitige Lese- und Schreibzugriffe sicher mithilfe von Mutexen und gewährleistet die Datenintegrität, wenn mehrere Vorgänge gleichzeitig stattfinden.
Du organisierst deinen Code auf zwei Dateien verteilt:
inventory.go: Definiere dein threadsicheres Bestandsverwaltungssystem.Erstelle eine
Inventory-Struktur, die Produktmengen in einer Map speichert und den Zugriff mithilfe einessync.RWMutexschützt. Dein Inventar soll die folgenden Vorgänge unterstützen:NewInventory() *Inventory– Erstellt ein neues Inventar mit einer initialisierten MapAddStock(product string, quantity int)– Fügt die Menge zum Bestand eines Produkts hinzu (verwende eine exklusive Sperre, da dadurch Daten geändert werden)GetStock(product string) int– Gibt den aktuellen Bestand eines Produkts zurück oder 0, falls es nicht gefunden wurde (verwende eine Lesesperre, da hierbei nur Daten gelesen werden)RemoveStock(product string, quantity int) bool– Entfernt die Menge aus dem Bestand, wenn genügend Bestand vorhanden ist. Gibttruezurück, wenn der Vorgang erfolgreich war, undfalse, wenn der Bestand nicht ausreicht (verwende eine exklusive Sperre)
Denke daran,
deferzum Entsperren zu verwenden, damit die Sperren immer ordnungsgemäß freigegeben werden.main.go: Lies die Vorgänge ein und demonstriere dein threadsicheres Inventar.Lies die Anzahl der Vorgänge ein und verarbeite anschließend jeden Vorgang. Jeder Vorgang besitzt einen Typ (
add,getoderremove), einen Produktnamen und beiadd- bzw.remove-Vorgängen eine Menge.Gib für jeden Vorgang das Ergebnis aus:
add: GibAdded [quantity] [product]ausget: Gib[product]: [stock] in stockausremove: Gib bei ErfolgRemoved [quantity] [product]aus, andernfallsInsufficient stock for [product]
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Anzahl der Vorgänge (Ganzzahl)
- Folgende Zeilen: Für jeden Vorgang:
- Vorgangstyp (
add,getoderremove) - Produktname
- Menge (nur für
add- undremove-Vorgänge)
- Vorgangstyp (
Beispielhaft wird Folgendes eingegeben:
5
add
apples
50
get
apples
remove
apples
30
remove
apples
25
get
applesDeine Ausgabe sollte wie folgt aussehen:
Added 50 apples
apples: 50 in stock
Removed 30 apples
Insufficient stock for apples
apples: 20 in stockDer RWMutex ermöglicht es mehreren GetStock-Aufrufen, gleichzeitig zu lesen, während AddStock und RemoveStock beim Ändern des Inventars exklusiven Zugriff erhalten.
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Anzahl der Operationen lesen
line, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(line))
// Neues Inventar erstellen
inventory := NewInventory()
// Jede Operation verarbeiten
for i := 0; i < numOps; i++ {
// Operationstyp lesen
opLine, _ := reader.ReadString('\n')
opType := strings.TrimSpace(opLine)
// Produktnamen lesen
productLine, _ := reader.ReadString('\n')
product := strings.TrimSpace(productLine)
// TODO: Jeden Operationstyp behandeln (add, get, remove)
// Für "add" und "remove" die Menge aus der Eingabe lesen
// Die entsprechende Inventar-Methode aufrufen
// Das Ergebnis gemäß der Challenge-Beschreibung ausgeben
switch opType {
case "add":
// TODO: Menge lesen, Bestand hinzufügen, Ergebnis ausgeben
case "get":
// TODO: Bestand abrufen, Ergebnis ausgeben
case "remove":
// TODO: Menge lesen, Bestand entfernen, entsprechendes Ergebnis ausgeben
}
}
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP in Go
Externe DateienGo-Workspace & ModulePackages & ImportsExportierte vs. nicht exportierte NamenEinführung in OOP mit GoStructs als KlassenMethoden für Structs definierenPointer- vs. Value-ReceiverStruct-InitialisierungKonstruktorfunktionenRückblick – Einfacher Taschenrechner4Schnittstellen
Einführung in SchnittstellenImplizite ImplementierungSchnittstelle als VertragLeere Schnittstelle (any)TypzusicherungTypwechselZusammensetzung von SchnittstellenStringer- und Error-SchnittstellenRückblick – Formenrechner7Kapselung
Exportierte vs. nicht exportierte FelderKapselung auf PaketebeneGetter- und Setter-MethodenInformationsverbergung in GoRückblick – Studierendendatensätze10Generics (Go 1.18+)
Einführung in GenericsTypparameterTypbeschränkungenGenerische StrukturenWorkaround für generische MethodenZusammenfassung – Generische Sammlung2Typen & Structs im Detail
Grundlegende & zusammengesetzte TypenBenutzerdefinierte TypdefinitionenStruct-TagsAnonyme StructsVerschachtelte StructsNullwerte & StandardwerteRückblick – Kontaktbuch5Komposition statt Vererbung
Warum Go keine Vererbung hatGrundlagen der Struct-EinbettungMethoden-PromotionMehrere Structs einbettenEinbettung vs. AggregationVerbergen eingebetteter MethodenRückblick – Mitarbeiterhierarchie8Fehlerbehandlung & OOP
Das Error-InterfaceBenutzerdefinierte FehlertypenError-Wrapping (fmt.Errorf)Sentinel-Fehlererrors.Is() und errors.As()Panic, Defer und RecoverRückblick – Dateiparser3Zeiger & Speicher
Grundlagen von Zeigern in GoZeiger auf StructsÜbergabe per Wert vs. ReferenzDie Funktion new()Garbage Collection in GoRückblick – Verkettete Liste erstellen6Polymorphismus in Go
Polymorphismus über InterfacesDuck-Typing in GoRegeln zur Interface-ErfüllungPolymorphe SammlungenDependency InjectionZusammenfassung – Zahlungsprozessor9Konkurrenz & OOP
Grundlagen der GoroutinesChannels & KommunikationGepufferte vs. ungepufferte ChannelsSelect-Anweisungsync.Mutex & sync.RWMutexsync.WaitGroupThread-sicheres Struct-DesignRückblick – Worker PoolÜbe selbstständig: Online-Go-Compiler