sync.Map.CompareAndSwap în Go

Schimbă (CompareAndSwap) sau șterge (CompareAndDelete) o cheie doar dacă valoarea ei este încă cea așteptată.

Sintaxă
func (m *Map) CompareAndSwap(key, old, new any) (swapped bool)
func (m *Map) CompareAndDelete(key, old any) (deleted bool)

Ce face sync.Map.CompareAndSwap

CompareAndSwap(k, vechi, nou) pune nou doar dacă valoarea curentă a cheii este egală cu vechi, și spune dacă a reușit. Așa eviți să suprascrii o modificare făcută între timp de alt goroutine: citești, calculezi, apoi scrii doar dacă nimeni nu a schimbat valoarea.

CompareAndDelete(k, vechi) șterge cheia doar dacă are valoarea vechi. Dacă cheia lipsește, ambele returnează false. Valoarea vechi trebuie să fie de un tip comparabil (numere, texte, pointeri), altfel apare panică.

Ce returnează

true dacă schimbarea sau ștergerea s-a făcut.

Exemple

Exemplul 1: Schimbarea stării unei comenzi

package main

import (
	"fmt"
	"sync"
)

func main() {
	var comenzi sync.Map
	comenzi.Store(101, "nouă")

	fmt.Println("nouă -> plătită:", comenzi.CompareAndSwap(101, "nouă", "plătită"))
	fmt.Println("nouă -> anulată:", comenzi.CompareAndSwap(101, "nouă", "anulată"))
	v, _ := comenzi.Load(101)
	fmt.Println("Stare:", v)

	fmt.Println("Șterg dacă e anulată:", comenzi.CompareAndDelete(101, "anulată"))
	fmt.Println("Șterg dacă e plătită:", comenzi.CompareAndDelete(101, "plătită"))
	_, exista := comenzi.Load(101)
	fmt.Println("Mai există:", exista)
}

Rezultat

nouă -> plătită: true
nouă -> anulată: false
Stare: plătită
Șterg dacă e anulată: false
Șterg dacă e plătită: true
Mai există: false

Exemplul 2: Contor de voturi cu reîncercare

Fiecare goroutine citește valoarea, o mărește și încearcă să o scrie. Dacă alt goroutine a fost mai rapid, CompareAndSwap eșuează și bucla încearcă din nou.

package main

import (
	"fmt"
	"sync"
)

func voteaza(m *sync.Map, candidat string) {
	for {
		v, _ := m.LoadOrStore(candidat, 0)
		if m.CompareAndSwap(candidat, v, v.(int)+1) {
			return
		}
	}
}

func main() {
	var voturi sync.Map
	var wg sync.WaitGroup
	for i := range 300 {
		wg.Go(func() {
			if i%3 == 0 {
				voteaza(&voturi, "Ion")
			} else {
				voteaza(&voturi, "Maria")
			}
		})
	}
	wg.Wait()
	ion, _ := voturi.Load("Ion")
	maria, _ := voturi.Load("Maria")
	fmt.Println("Ion:", ion, "Maria:", maria)
}

Rezultat

Ion: 100 Maria: 200

Greșeli frecvente

  • Folosești CompareAndSwap cu valori slice sau map: nu sunt comparabile și programul se oprește cu panică.
  • Nu verifici rezultatul bool. Dacă schimbarea nu s-a făcut, modificarea ta s-a pierdut; reîncearcă sau tratează cazul.

Comenzi înrudite

  • sync.Map: Un map sigur pentru goroutine fără mutex extern: Store, Load, Delete, Range și Clear pot fi apelate concurent.
  • sync.Map.LoadOrStore: Operații combinate fără cursă de date: LoadOrStore pune doar dacă lipsește, LoadAndDelete scoate, Swap înlocuiește.
  • sync/atomic.CompareAndSwapInt64: CompareAndSwapInt64 scrie doar dacă valoarea e cea așteptată; SwapInt64 scrie și returnează valoarea veche.