sync.Mutex în Go

Lacăt de excludere reciprocă: doar un goroutine pe rând poate rula codul dintre Lock() și Unlock().

Sintaxă
import "sync"

var mu sync.Mutex
func (m *Mutex) Lock()
func (m *Mutex) Unlock()

Ce face sync.Mutex

Când mai multe goroutine modifică aceeași variabilă (un contor, un map, un slice), rezultatul poate ieși greșit sau programul se poate opri cu eroarea concurrent map writes. sync.Mutex rezolvă problema: mu.Lock() „încuie” lacătul, iar dacă alt goroutine îl ține deja, așteaptă până când acela apelează mu.Unlock(). Codul dintre cele două apeluri (secțiunea critică) rulează deci de un singur goroutine o dată.

Valoarea zero este un mutex descuiat, gata de folosit: nu trebuie inițializat. Modelul obișnuit este mu.Lock() urmat imediat de defer mu.Unlock(), ca lacătul să fie eliberat și dacă funcția iese mai devreme. De regulă mutexul stă în aceeași structură cu datele pe care le protejează.

Un mutex nu se copiază după prima folosire. Structurile care conțin un mutex se transmit prin pointer, iar metodele lor au receptor pointer (func (c *Cont) ...). go vet semnalează copierile greșite.

Ce returnează

Lock și Unlock nu returnează nimic. Unlock pe un mutex descuiat oprește programul cu eroare fatală.

Exemple

Exemplul 1: Contor sigur pentru 100 de goroutine

Fiecare goroutine adaugă 1000 la contor. Cu mutex, rezultatul este mereu exact 100000.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var (
		mu     sync.Mutex
		contor int
		wg     sync.WaitGroup
	)
	for range 100 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for range 1000 {
				mu.Lock()
				contor++
				mu.Unlock()
			}
		}()
	}
	wg.Wait()
	fmt.Println("Contor:", contor)
}

Rezultat

Contor: 100000

Exemplul 2: Cont bancar protejat de mutex

Mutexul stă în structură, metodele au receptor pointer și folosesc defer pentru Unlock. Retragerile care ar duce soldul pe minus sunt refuzate corect chiar dacă vin în paralel.

package main

import (
	"fmt"
	"sync"
)

type Cont struct {
	mu   sync.Mutex
	sold int
}

func (c *Cont) Retrage(suma int) bool {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.sold < suma {
		return false
	}
	c.sold -= suma
	return true
}

func (c *Cont) Sold() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.sold
}

func main() {
	cont := &Cont{sold: 1000}
	var wg sync.WaitGroup
	var mu sync.Mutex
	reusite := 0
	for range 15 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			if cont.Retrage(100) {
				mu.Lock()
				reusite++
				mu.Unlock()
			}
		}()
	}
	wg.Wait()
	fmt.Println("Retrageri reușite:", reusite)
	fmt.Println("Sold final:", cont.Sold(), "lei")
}

Rezultat

Retrageri reușite: 10
Sold final: 0 lei

Exemplul 3: Fără mutex: rezultat greșit

Același contor fără lacăt. contor++ înseamnă citește, adună, scrie; două goroutine pot citi aceeași valoare și o adunare se pierde. Rezultatul diferă la fiecare rulare, iar go run -race raportează problema.

package main

import (
	"fmt"
	"sync"
)

func main() {
	contor := 0
	var wg sync.WaitGroup
	for range 100 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for range 1000 {
				contor++ // cursă de date!
			}
		}()
	}
	wg.Wait()
	fmt.Println("Contor:", contor)
}

Rezultat posibil (diferă la fiecare rulare)

Contor: 38517

Greșeli frecvente

  • Uiți Unlock pe un drum de ieșire (un return într-un if). Toate celelalte goroutine rămân blocate; folosește defer mu.Unlock().
  • Apelezi Lock de două ori din același goroutine (de exemplu o metodă blocată apelează altă metodă care blochează același mutex). Mutexul din Go nu este reentrant: programul se blochează.
  • Copiezi o structură cu mutex (receptor valoare func (c Cont) sau transmitere prin valoare). Fiecare copie are lacătul ei și protecția dispare.

Comenzi înrudite

  • sync.RWMutex: Lacăt pentru citire și scriere: mai mulți cititori pot lucra deodată (RLock), dar un scriitor lucrează singur (Lock).
  • sync.Mutex.TryLock: Încearcă să încuie mutexul fără să aștepte: returnează true dacă a reușit, false dacă lacătul este ocupat.
  • sync.WaitGroup: Așteaptă ca un grup de goroutine să termine: Add mărește contorul, Done îl scade, Wait blochează până ajunge la zero.
  • sync/atomic.Int64: Un int64 cu operații atomice ca metode (Add, Load, Store, Swap); varianta recomandată pentru contoare între goroutine.