sync.RWMutex în Go

Lacăt pentru citire și scriere: mai mulți cititori pot lucra deodată (RLock), dar un scriitor lucrează singur (Lock).

Sintaxă
var rw sync.RWMutex
func (rw *RWMutex) RLock()      // citire, mai mulți deodată
func (rw *RWMutex) RUnlock()
func (rw *RWMutex) Lock()       // scriere, exclusiv
func (rw *RWMutex) Unlock()
func (rw *RWMutex) RLocker() Locker

Ce face sync.RWMutex

sync.RWMutex are două feluri de blocare. RLock/RUnlock sunt pentru citire: oricâte goroutine pot citi în același timp. Lock/Unlock sunt pentru scriere: scriitorul așteaptă să termine toți cititorii și nimeni altcineva nu intră cât timp scrie.

Merită folosit când datele sunt citite des și modificate rar: un cache, o configurare, o listă de prețuri. Dacă scrierile sunt dese, un sync.Mutex simplu este la fel de bun și mai ușor de înțeles.

rw.RLocker() returnează un sync.Locker ale cărui Lock/Unlock apelează de fapt RLock/RUnlock. Este util când o funcție primește un sync.Locker oarecare și vrei să-i dai partea de citire.

Ce returnează

Metodele de blocare nu returnează nimic. RLocker returnează un sync.Locker.

Exemple

Exemplul 1: Cache de prețuri: mulți cititori, un scriitor

package main

import (
	"fmt"
	"sync"
)

type Preturi struct {
	mu   sync.RWMutex
	date map[string]float64
}

func (p *Preturi) Pret(produs string) (float64, bool) {
	p.mu.RLock()
	defer p.mu.RUnlock()
	v, ok := p.date[produs]
	return v, ok
}

func (p *Preturi) Seteaza(produs string, pret float64) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.date[produs] = pret
}

func main() {
	p := &Preturi{date: map[string]float64{"pâine": 4.5, "lapte": 7.2}}
	var wg sync.WaitGroup
	gasite := make([]float64, 10)
	for i := range 10 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			gasite[i], _ = p.Pret("lapte")
		}()
	}
	wg.Wait()
	fmt.Println("10 citiri în paralel:", gasite)

	p.Seteaza("lapte", 7.9)
	v, _ := p.Pret("lapte")
	fmt.Println("După scumpire:", v)
	_, ok := p.Pret("cafea")
	fmt.Println("Cafea în listă:", ok)
}

Rezultat

10 citiri în paralel: [7.2 7.2 7.2 7.2 7.2 7.2 7.2 7.2 7.2 7.2]
După scumpire: 7.9
Cafea în listă: false

Exemplul 2: RLocker: partea de citire ca sync.Locker

Funcția cuLacat primește orice sync.Locker. Cu RLocker(), două apeluri pot ține lacătul de citire în același timp.

package main

import (
	"fmt"
	"sync"
)

func cuLacat(l sync.Locker, f func()) {
	l.Lock()
	defer l.Unlock()
	f()
}

func main() {
	var rw sync.RWMutex
	cititor := rw.RLocker()

	cuLacat(cititor, func() {
		// suntem în RLock: un alt cititor poate intra, un scriitor nu
		fmt.Println("Al doilea cititor poate intra:", rw.TryRLock())
		rw.RUnlock()
		fmt.Println("Un scriitor poate intra:     ", rw.TryLock())
	})
	cuLacat(&rw, func() {
		fmt.Println("Acum scriem, cititorii așteaptă:", !rw.TryRLock())
	})
}

Rezultat

Al doilea cititor poate intra: true
Un scriitor poate intra:      false
Acum scriem, cititorii așteaptă: true

Greșeli frecvente

  • Modifici datele sub RLock. Cititorii lucrează în paralel, deci scrierea sub RLock este tot o cursă de date; pentru modificări folosește Lock.
  • Încerci să „promovezi” un RLock în Lock fără să eliberezi întâi citirea: Lock așteaptă să plece toți cititorii, inclusiv tu, și programul se blochează.
  • Apelezi RLock recursiv (o funcție cu RLock apelează alta care face tot RLock). Dacă între timp așteaptă un scriitor, al doilea RLock se blochează.

Comenzi înrudite

  • sync.Mutex: Lacăt de excludere reciprocă: doar un goroutine pe rând poate rula codul dintre Lock() și Unlock().
  • 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.Map: Un map sigur pentru goroutine fără mutex extern: Store, Load, Delete, Range și Clear pot fi apelate concurent.
  • sync/atomic.Pointer: Un pointer *T citit și înlocuit atomic, cu tip verificat la compilare; varianta modernă a lui atomic.Value.