Sobes.tech
Middle+

Czym są operacje atomowe, jakie są ich rodzaje i kiedy oraz jak ich najlepiej używać?

sobes.tech AI

Odpowiedź od AI

Operacje atomowe to operacje na pamięci, których wykonanie gwarantuje, że albo zostaną one w pełni zakończone, albo w ogóle się nie rozpoczną, i nie mogą być przerwane przez inne konkurujące operacje. Są niepodzielne i bezpieczne dla wątków na poziomie sprzętowym.

W Go, pakiet sync/atomic zapewnia zestaw funkcji do wykonywania operacji atomowych:

Funkcja Opis
AddInt32 Atomowo dodaje wartość do int32.
AddInt64 Atomowo dodaje wartość do int64.
AddUint32 Atomowo dodaje wartość do uint32.
AddUint64 Atomowo dodaje wartość do uint64.
AddUintptr Atomowo dodaje wartość do uintptr.
CompareAndSwapInt32 Porównuje i zamienia (CAS) int32 atomowo.
CompareAndSwapInt64 Porównuje i zamienia (CAS) int64 atomowo.
CompareAndSwapUint32 Porównuje i zamienia (CAS) uint32.
CompareAndSwapUint64 Porównuje i zamienia (CAS) uint64.
CompareAndSwapUintptr Porównuje i zamienia (CAS) uintptr.
CompareAndSwapPointer Porównuje i zamienia (CAS) unsafe.Pointer.
LoadInt32 Atomowo ładuje wartość int32.
LoadInt64 Atomowo ładuje wartość int64.
LoadUint32 Atomowo ładuje wartość uint32.
LoadUint64 Atomowo ładuje wartość uint64.
LoadUintptr Atomowo ładuje wartość uintptr.
LoadPointer Atomowo ładuje wartość unsafe.Pointer.
StoreInt32 Atomowo zapisuje wartość int32.
StoreInt64 Atomowo zapisuje wartość int64.
StoreUint32 Atomowo zapisuje wartość uint32.
StoreUint64 Atomowo zapisuje wartość uint64.
StoreUintptr Atomowo zapisuje wartość uintptr.
StorePointer Atomowo zapisuje wartość unsafe.Pointer.
SwapInt32 Atomowo zamienia wartości int32.
SwapInt64 Atomowo zamienia wartości int64.
SwapUint32 Atomowo zamienia wartości uint32.
SwapUint64 Atomowo zamienia wartości uint64.
SwapUintptr Atomowo zamienia wartości uintptr.
SwapPointer Atomowo zamienia wartości unsafe.Pointer.

Zaleca się używanie operacji atomowych w przypadkach, gdy wymagana jest prosta, niskopoziomowa synchronizacja dostępu do wspólnych zmiennych, najczęściej liczników lub flag, bez użycia mutexów. Zapewniają minimalne koszty i lepszą wydajność w takich scenariuszach.

Przykład użycia atomowego licznika:

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

func main() {
	var counter int64
	var wg sync.WaitGroup

	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func() {
			// Atomowo zwiększamy licznik
			atomic.AddInt64(&counter, 1)
			wg.Done()
		}()
	}

	wg.Wait()
	// Atomowo ładujemy wartość licznika
	fmt.Println("Ostateczny licznik:", atomic.LoadInt64(&counter))
}

Przykład użycia CompareAndSwap:

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var value int32 = 10

	// Próba zamiany wartości, jeśli obecnie jest 10, na 20
	swapped := atomic.CompareAndSwapInt32(&value, 10, 20)
	fmt.Println("Zamiana:", swapped) // true
	fmt.Println("Wartość:", value)   // 20

	// Próba zamiany wartości, jeśli obecnie jest 10, na 30
	swapped = atomic.CompareAndSwapInt32(&value, 10, 30)
	fmt.Println("Zamiana:", swapped) // false
	fmt.Println("Wartość:", value)   // 20
}

Ważne jest, aby pamiętać, że operacje atomowe działają tylko na typach prostych (liczbach całkowitych, wskaźnikach). Do synchronizacji dostępu do bardziej złożonych struktur danych lub wykonywania operacji blokujących nadal potrzebne są mutexy lub inne mechanizmy synchronizacji. Nadmierne lub niewłaściwe użycie operacji atomowych może prowadzić do trudnych do wykrycia błędów lub obniżenia wydajności.