# Nízkourovňová synchronizace v Go: atomiky, bariéry a přeuspořádání instrukcí
Pochopení mechanismů synchronizace na úrovni procesoru není akademickým luxusem, ale praktickou nezbytností pro vývojáře vícevláknových aplikací v Go. V tomto článku rozebereme, jak CPU optimalizuje provádění kódu, proč to narušuje očekávanou logiku v konkurenčních programech a jaké hardwarové primitivy pomáhají obnovit pořadí.
Jak procesor oklamech programátora: reordering a out-of-order execution
Když napíšete x = 1; ready = true, očekáváte striktní posloupnost. Procesor ne. Přeuspořádává nezávislé instrukce, aby vyplnil prostoje v pipeline. To se nazývá instruction reordering – přeuspořádání instrukcí. Cílem je maximalizovat využití výkonných bloků a skrýt zpoždění paměti.
CPU využívá out-of-order execution: instrukce se provádějí ne podle pořadí v kódu, ale podle připravenosti jejich operandů. Například:
MOVQ A(SB), AX ; načtení z paměti
MOVQ B(SB), BX ; načtení z paměti
ADDQ DX, CX ; sčítání registrů
Procesor může začít s ADDQ, protože operandy jsou už v registrech, a načítání poslat do pozadí – ta zaberou stovky taktů. Logický výsledek pro aktuální vlákno se nezmění, ale pro ostatní vlákna ano.
Etapy pipeline, kde vzniká chaos:
- Fetch → Decode → Register Rename → Dispatch → Execute → Commit
Klíčový moment je etapa Dispatch. Zde plánovač vybírá instrukce s připravenými operandy a ignoruje původní pořadí. Reorder Buffer (ROB) zaručuje, že commit instrukcí do architekturálního stavu proběhne ve správném pořadí – ale jen pro aktuální jádro.
Memory reordering: kdy se store buffer stane zrádcem
Problém se zhoršuje na úrovni paměti. Zápisy do RAM neprobíhají okamžitě – putují do store bufferu a odtud se asynchronně srážejí do cache. Pro jedno jádro:
x = 1
ready = true
Pro jiné jádro může být vidět takto:
ready = true
x = 0
Protože zápis ready=true mohl store buffer opustit dříve. To je memory reordering – přeuspořádání paměťových operací, viditelné pro ostatní vlákna. Klasický příklad závodnosti:
G1:
x = 1
ready = true
G2:
if ready {
print(x) // může vypsat 0!
}
Paměťové bariéry: MFENCE a další nástroje pro obnovení pořadí
Řešením jsou memory fences (paměťové bariéry). Jedná se o instrukce, které vynuceně synchronizují viditelnost operací mezi jádry. Na x86 je hlavní bariérou MFENCE:
MOVQ $1, x(SB)
MFENCE ; bariéra
MOVQ $1, ready(SB)
MFENCE zaručuje, že všechny zápisové operace před ním budou vidět všemi jádry před zahájením operací po něm. Zakazuje čtyři typy přeuspořádání:
- Store → Store
- Load → Load
- Load → Store
- Store → Load
Důležité: x86 má relativně striktní model paměti – mnohá přeuspořádání jsou již hardwarově zakázána. Ale Store→Load je stále možný, proto je MFENCE nutný pro striktní synchronizaci.
Seznam typů bariér na různých architekturách:
- x86: MFENCE (plná), SFENCE (store→store), LFENCE (load→load)
- ARM: DMB (Data Memory Barrier), ISH (inner shareable domain)
- RISC-V: FENCE (flexibilní, s určením směrů)
Atomové operace: vysoceúrovňová abstrakce nad LOCK
Go schovává bariéry za atomovými operacemi z balíčku sync/atomic. Pod kapotou využívají instrukci LOCK, která činí read-modify-write operace nedělitelnými pro všechna jádra. Příklad XADDQ:
TEXT ·Xadd64(SB), NOSPLIT, $0-24
MOVQ ptr+0(FP), BX
MOVQ delta+8(FP), AX
MOVQ AX, CX
LOCK
XADDQ AX, 0(BX) ; atomically: tmp=*ptr; *ptr+=AX; AX=tmp
ADDQ CX, AX
MOVQ AX, ret+16(FP)
RET
LOCK funguje přes blokování cache line v protokolu MESI:
- Jádro získá exkluzivní právo na cache řádek
- Provede operaci
- Uvolní řádek – změny se stanou viditelnými pro ostatní jádra
Atomovost ≠ uspořádanost! Atomová operace zaručuje nedělitelnost, ale ne nutně vytváří memory barrier. Na x86 jsou jednoduché load/store atomové, ale nezaručují ordering.
Píšeme vlastní atomový AND: praxe v Go Assembleru
Standardní knihovna Go neposkytuje atomic.And. Napišeme si to sami. Vytvoříme soubor Xand8_x86.s:
#include "textflag.h"
TEXT ·And8(SB), NOSPLIT, $0-9
MOVQ ptr+0(FP), BX // BX = ukazatel
MOVB mask+8(FP), AX // AX = maska (8 bitů)
LOCK
ANDB AX, 0(BX) // *ptr &= mask
RET
A odpovídající main.go:
package main
import "fmt"
func And8(ptr *uint8, mask uint8)
func main() {
var smth uint8 = 3
And8(&smth, uint8(2))
fmt.Println("Výsledek:", smth) // vypíše 2
}
Kompilujeme a spouštíme přes go run . – ASM soubor se automaticky načte. Všimněte si: používáme ANDB s prefixem LOCK, i když na x86 neexistuje XAND. To je v pořádku – LOCK je kompatibilní s základními logickými operacemi.
Co je důležité
- Instruction reordering – optimalizace CPU, která mění pořadí provádění nezávislých instrukcí pro zvýšení výkonu.
- Memory reordering – nesynchronizace viditelnosti zápisových operací mezi jádry kvůli store bufferu.
- Memory fences (bariéry) – instrukce, které vynuceně synchronizují pořadí paměťových operací mezi vlákny.
- Atomové operace – nedělitelné read-modify-write akce, realizované přes LOCK a obalující bariéry.
- LOCK + ANDB – funkční způsob realizace atomové bitwise operace, chybějící ve standardní knihovně Go.
— Editorial Team
Zatím žádné komentáře.