sync.Mutex & sync.RWMutex
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy. Lektion 63 von 107.
Während Channels der von Go bevorzugte Weg 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, werden blockiert, 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, während Schreiber exklusiven Zugriff erhalten:
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 Lesevorgänge und Lock()/Unlock() für Schreibvorgänge. Dadurch werden gleichzeitige Lesevorgänge ermöglicht und gleichzeitig sichere Schreibvorgänge gewährleistet.
Aufgabe
EinfachBauen wir ein threadsicheres Inventarsystem, das die Lagerbestände von Produkten erfasst. Dein System verarbeitet gleichzeitige Lese- und Schreibvorgänge mithilfe von Mutexen sicher 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 dem Bestand eines Produkts eine Menge hinzu (verwende eine exklusive Sperre, da dadurch Daten verändert werden)GetStock(product string) int– Gibt den aktuellen Bestand eines Produkts zurück oder 0, wenn es nicht gefunden wurde (verwende eine Lesesperre, da hierbei nur Daten gelesen werden)RemoveStock(product string, quantity int) bool– Entfernt eine Menge aus dem Bestand, wenn ausreichend Bestand vorhanden ist. Gibttruebei Erfolg undfalsebei unzureichendem Bestand zurück (verwende eine exklusive Sperre)
Denke daran, zum Entsperren
deferzu 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 bei Vorgängen vom Typadd/removeeine 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 Vorgänge vom Typ
addundremove)
- Vorgangstyp (
Beispiel: Bei folgender Eingabe:
5
add
apples
50
get
apples
remove
apples
30
remove
apples
25
get
applessollte deine Ausgabe 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)
// Lese die Anzahl der Operationen
line, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(line))
// Erstelle ein neues Inventar
inventory := NewInventory()
// Verarbeite jede Operation
for i := 0; i < numOps; i++ {
// Lese den Operationstyp
opLine, _ := reader.ReadString('\n')
opType := strings.TrimSpace(opLine)
// Lese den Produktnamen
productLine, _ := reader.ReadString('\n')
product := strings.TrimSpace(productLine)
// TODO: Behandle jeden Operationstyp (add, get, remove)
// Für "add" und "remove" die Menge aus der Eingabe lesen
// Rufe die entsprechende Inventar-Methode auf
// Gib das Ergebnis gemäß der Challenge-Beschreibung aus
switch opType {
case "add":
// TODO: Menge lesen, Bestand hinzufügen, Ergebnis ausgeben
case "get":
// TODO: Bestand holen, 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