Menu

Golang Mutex: sync.Mutex, RWMutex, atomic und sync.Once

So schützt du geteilten Zustand zwischen Goroutinen mit sync.Mutex und sync.RWMutex, wann sync/atomic reicht, wie sync.Once die Initialisierung genau einmal ausführt und welche Fehler beim Sperren zu Deadlocks führen.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Geteilte Daten schützen

Ein sync.Mutex lässt immer nur eine Goroutine in den Code zwischen Lock und Unlock. Setz den Mutex neben die Daten, die er schützt, meist in dasselbe Struct:

Das gibt immer hits: 10000 aus. Ohne das Lock würden 100 Goroutinen, die dieselbe Map schreiben, das Programm meist mit fatal error: concurrent map writes abstürzen lassen. Dieser Fehler kommt von einer Prüfung der Runtime, die nach bestem Bemühen arbeitet, er lässt sich nicht abfangen, und das Ausbleiben eines Absturzes beweist nicht, dass der Code korrekt ist.

Details, die zählen:

  • Der Nullwert von sync.Mutex ist entsperrt und einsatzbereit. Kein Konstruktor nötig.
  • Methoden verwenden einen Pointer Receiver (*Counter). Ein Value Receiver würde eine Kopie des Mutex sperren, und die schützt nichts.
  • defer c.mu.Unlock() direkt nach Lock sorgt dafür, dass jeder Return-Pfad und auch eine Panic das Lock freigibt.
  • Jeder Zugriff läuft über das Lock, auch Lesezugriffe. Ein Lesen ohne Lock, während eine andere Goroutine schreibt, bleibt ein Data Race.

Den kritischen Abschnitt klein halten

defer entsperrt am Ende der Funktion. Für kurze Methoden wie die oben ist das richtig. In einer längeren Funktion entsperrst du, sobald die geteilten Daten nicht mehr angefasst werden, damit andere Goroutinen nicht auf Arbeit warten, die das Lock gar nicht braucht:

func (s *Store) Save(key string) error {
	s.mu.Lock()
	data := s.items[key] // copy what you need
	s.mu.Unlock()

	return writeToDisk(key, data) // slow I/O, outside the lock
}

Ein Lock über Netzwerkaufrufe, Festplatten-I/O oder das Senden auf einen Channel hinweg zu halten ist die häufigste Ursache für ein langsames nebenläufiges Programm, und ein Channel-Send unter einem Lock ist eine häufige Ursache für Deadlocks.

RWMutex für leseintensive Daten

sync.RWMutex hat zwei Modi. RLock/RUnlock nehmen ein geteiltes Lese-Lock, das viele Goroutinen gleichzeitig halten können. Lock/Unlock nehmen das exklusive Schreib-Lock, das wartet, bis alle Leser weg sind.

RWMutex lohnt sich, wenn Lesezugriffe dominieren und jeder davon unter dem Lock echte Arbeit leistet. Bei winzigen kritischen Abschnitten wie einem einzelnen Map-Lookup ist ein einfacher Mutex oft genauso schnell, weil das Lese-Lock eigene Verwaltung mitbringt. Mach vor der Entscheidung einen Benchmark.

Ein Lese-Lock lässt sich nicht zu einem Schreib-Lock hochstufen. Lock aufzurufen, während dieselbe Goroutine RLock hält, führt zum Deadlock. Gib zuerst das Lese-Lock frei, nimm dann das Schreib-Lock und prüf die Bedingung erneut, da ein anderer Schreiber die Daten inzwischen geändert haben kann.

sync/atomic für einzelne Werte

Für einen einzelnen Zähler oder ein Flag ist sync/atomic einfacher und billiger als ein Mutex. Die typisierten Wrapper (Go 1.19) sind die richtige Wahl:

Atomics schützen jeweils einen Wert. Sobald sich zwei Werte gemeinsam ändern müssen (ein Kontostand und eine Anzahl Transaktionen, eine Map und ihre Größe), nimm einen Mutex. Zwischen zwei separaten atomaren Operationen können sich andere Goroutinen einschieben.

sync.Once

sync.Once führt eine Funktion genau einmal aus, egal wie viele Goroutinen sie gleichzeitig aufrufen. Jeder, der Do aufruft, wartet, bis der erste Aufruf fertig ist. Das ist der Standardweg für eine verzögerte Initialisierung:

Jede „runs once“-Zeile erscheint genau einmal. Löst die an Do übergebene Funktion eine Panic aus, zählt Once sie trotzdem als erledigt und versucht es nie erneut. sync.OnceValues macht dasselbe für Funktionen, die zwei Werte zurückgeben, typischerweise einen Wert und einen Fehler.

Mutex, Channel oder sync.Map

SituationNimm
Ein Struct oder eine Map, die mehrere Goroutinen direkt ändernsync.Mutex im Struct
Überwiegend Lesezugriffe, gelegentlich Schreiben, Lesen macht echte Arbeitsync.RWMutex
Ein einzelner Zähler oder ein Flagsync/atomic
Einmalige Initialisierungsync.Once, sync.OnceValue
Daten von einer Goroutine an eine andere übergebenein Channel
Ein Cache, dessen Schlüssel einmal geschrieben und oft gelesen werden, oder Goroutinen mit getrennten Schlüsselnsync.Map

sync.Map ist kein allgemeiner Ersatz für eine Map mit Lock. Es hat keine Typparameter, Werte kommen also als any zurück, und es ist nur in den beiden Fällen aus der Tabelle schneller. Fang mit einem Mutex und einer normalen Map an.

Häufige Fehler

  • Einen Mutex kopieren. Ein Struct mit einem sync.Mutex als Wert zu übergeben oder einen Value Receiver zu verwenden kopiert das Lock. go vet meldet passes lock by value oder copies lock value.
  • Zweimal in einer Goroutine sperren. Go-Mutexe sind nicht reentrant. Ruft Inc Get auf und nehmen beide das Lock, blockiert Inc für immer. Lass öffentliche Methoden sperren und private Helfer davon ausgehen, dass das Lock gehalten wird.
  • Das Entsperren bei einem frühen Return vergessen. Nimm defer, außer du hast einen Grund dagegen.
  • In unterschiedlicher Reihenfolge sperren. Nimmt eine Goroutine Lock A und dann B, während eine andere B und dann A nimmt, können beide ewig aufeinander warten. Nimm mehrere Locks immer in derselben Reihenfolge.
  • Die geschützten Daten herausgeben. Gibt eine Methode die interne Map zurück, können Aufrufer sie ohne Lock lesen und schreiben. Gib eine Kopie zurück (maps.Clone, Go 1.21) oder einen einzelnen Wert.
  • Nur die Schreibzugriffe schützen. Lesezugriffe ohne Lock parallel zu Schreibzugriffen mit Lock sind trotzdem Races. Führ deine Tests mit go test -race aus.

Häufig gestellte Fragen

Was ist ein Mutex in Go?

Ein sync.Mutex ist ein Lock, das immer nur eine Goroutine den Code zwischen mu.Lock() und mu.Unlock() ausführen lässt. Du nutzt es, um Daten zu schützen, die mehrere Goroutinen lesen und schreiben, etwa eine Map oder ein Struct. Sein Nullwert ist ein entsperrter Mutex, sofort einsatzbereit.

Wann sollte ich RWMutex statt Mutex verwenden?

Wenn Lesezugriffe die Schreibzugriffe deutlich überwiegen und jeder Lesezugriff das Lock für eine nennenswerte Zeit hält. RLock lässt beliebig viele Leser gleichzeitig hinein, während Lock auf exklusiven Zugriff wartet. Bei kurzen kritischen Abschnitten ist ein einfacher Mutex oft genauso schnell oder schneller, also miss, bevor du wechselst.

Ist eine Go-Map sicher für nebenläufige Nutzung?

Nein. Nebenläufige Lesezugriffe sind in Ordnung, aber ein Schreibzugriff parallel zu irgendeinem anderen Lese- oder Schreibzugriff ist ein Data Race, und die Runtime erkennt es meist und stürzt mit fatal error: concurrent map writes (oder concurrent map read and map write) ab. Schütze die Map mit einem sync.Mutex oder sync.RWMutex oder nutze sync.Map für dessen spezielle Anwendungsfälle.

Ist sync.Mutex in Go reentrant?

Nein. Ruft eine Goroutine, die das Lock hält, erneut Lock auf, blockiert sie für immer und wartet auf sich selbst. Strukturier den Code so, dass exportierte Methoden das Lock nehmen und nicht exportierte Helfer aufrufen, die davon ausgehen, dass es schon gehalten wird.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S