Zpět na domů

Synchronizace v Go: atomiky a bariéry paměti

Rozbor mechanismů synchronizace na úrovni procesoru: instruction a memory reordering, memory fences, atomární operace. Praktický příklad implementace atomic.And na Go Assembler.

Nízkourovňová synchronizace v Go: od CPU po atomiky
Advertisement 728x90

# 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:

Google AdInline article slot
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.

Google AdInline article slot

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:

Google AdInline article slot
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

Advertisement 728x90

Číst dál