Volver al inicio

Sincronización en Go: operaciones atómicas y barreras de memoria

Análisis de mecanismos de sincronización a nivel de procesador: reordenamiento de instrucciones y memoria, vallas de memoria, operaciones atómicas. Ejemplo práctico de implementación de atomic.And en Ensamblador de Go.

Sincronización de bajo nivel en Go: desde la CPU hasta las operaciones atómicas
Advertisement 728x90

# Sincronización de Bajo Nivel en Go: Atómicas, Barreras y Reordenamiento de Instrucciones

Comprender los mecanismos de sincronización a nivel de procesador no es un lujo académico, sino una necesidad práctica para los desarrolladores que crean aplicaciones Go multihilo. En este artículo, desglosamos cómo la CPU optimiza la ejecución de código, por qué esto rompe la lógica esperada en programas concurrentes y qué primitivas de hardware ayudan a restaurar el orden.

Cómo el Procesador Engaña al Programador: Reordenamiento y Ejecución Fuera de Orden

Cuando escribes x = 1; ready = true, esperas un orden secuencial estricto. ¿La CPU? Nada de eso. Reordena instrucciones independientes para llenar paradas en el pipeline. Esto se llama instruction reordering. El objetivo: maximizar la utilización de las unidades de ejecución y ocultar latencias de memoria.

Las CPU usan out-of-order execution: las instrucciones se ejecutan no en el orden del programa, sino cuando sus operandos están listos. Por ejemplo:

Google AdInline article slot
MOVQ A(SB), AX ; load from memory
MOVQ B(SB), BX ; load from memory
ADDQ DX, CX    ; add registers

La CPU puede empezar con ADDQ porque los operandos ya están en registros, mientras envía las cargas al fondo: tardarán cientos de ciclos. El resultado lógico para el hilo actual sigue siendo el mismo, pero otros hilos podrían ver algo diferente.

Etapas del pipeline donde surge el caos:

  • Fetch → Decode → Register Rename → Dispatch → Execute → Commit

Etapa clave: Dispatch. Aquí, el programador elige instrucciones con operandos listos, ignorando el orden original. El Reorder Buffer (ROB) asegura que los commits de instrucciones al estado arquitectónico ocurran en la secuencia correcta, pero solo para el núcleo actual.

Google AdInline article slot

Reordenamiento de Memoria: Cuando el Buffer de Escritura te Traiciona

El problema empeora a nivel de memoria. Las escrituras en RAM no son instantáneas: caen en el store buffer y luego se vacían de forma asíncrona en caché. Para un núcleo:

x = 1
ready = true

Para otro núcleo, podría verse así:

ready = true
x = 0

Porque la escritura ready=true podría salir del store buffer antes. Esto es memory reordering: reordenamiento de operaciones de memoria visible para otros hilos. Ejemplo clásico de data race:

Google AdInline article slot
G1: 
x = 1
ready = true

G2:
if ready {
  print(x) // might print 0!
}

Barreras de Memoria: MFENCE y Otras Herramientas para Restaurar el Orden

Solución: memory fences (barreras de memoria). Estas instrucciones fuerzan la sincronización de la visibilidad de operaciones entre núcleos. En x86, la barrera principal es MFENCE:

MOVQ $1, x(SB)
MFENCE           ; barrier
MOVQ $1, ready(SB)

MFENCE garantiza que todas las stores anteriores sean visibles para todos los núcleos antes de que las operaciones posteriores comiencen a ejecutarse. Prohíbe cuatro tipos de reordenamiento:

  • Store → Store
  • Load → Load
  • Load → Store
  • Store → Load

Importante: x86 tiene un modelo de memoria relativamente fuerte; muchos reordenamientos ya están prohibidos por hardware. Pero Store→Load sigue siendo posible, por lo que MFENCE es esencial para sincronización estricta.

Tipos de barreras en diferentes arquitecturas:

  • x86: MFENCE (completa), SFENCE (store→store), LFENCE (load→load)
  • ARM: DMB (Data Memory Barrier), ISH (inner shareable domain)
  • RISC-V: FENCE (flexible, con especificadores de dirección)

Operaciones Atómicas: Abstracción de Alto Nivel sobre LOCK

Go oculta las barreras detrás de operaciones atómicas del paquete sync/atomic. Por debajo, usan el prefijo LOCK, que hace que las operaciones read-modify-write sean indivisibles en todos los núcleos. Ejemplo 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 funciona bloqueando la línea de caché en el protocolo MESI:

  • El núcleo adquiere derechos exclusivos a la línea de caché
  • Realiza la operación
  • Libera la línea: los cambios se vuelven visibles para otros núcleos

Atomicidad ≠ ordenamiento. Una operación atómica garantiza indivisibilidad, pero no necesariamente crea una barrera de memoria. En x86, load/store simples son atómicas pero no proporcionan ordenamiento.

Escribiendo Nuestra Propia Atómica AND: En Práctica con el Ensamblador de Go

La biblioteca estándar de Go no proporciona atomic.And. Implementémosla nosotros mismos. Crea el archivo Xand8_x86.s:

#include "textflag.h"

TEXT ·And8(SB), NOSPLIT, $0-9
	MOVQ	ptr+0(FP), BX      // BX = pointer
	MOVB	mask+8(FP), AX     // AX = mask (8 bits)
	LOCK
	ANDB	AX, 0(BX)          // *ptr &= mask
	RET

Y el main.go correspondiente:

package main

import "C"

func And8(ptr *uint8, mask uint8)

func main() {
	var smth uint8 = 3
	And8(&smth, uint8(2))
	println("Result:", smth) // outputs 2
}

Compila y ejecuta con go run .: el archivo ASM se detectará automáticamente. Nota: usamos ANDB con el prefijo LOCK, aunque x86 no tiene instrucción XAND. Funciona: LOCK es compatible con operaciones lógicas básicas.

Puntos Clave

  • Instruction reordering — Optimización de la CPU que cambia el orden de ejecución de instrucciones independientes para mejorar el rendimiento.
  • Memory reordering — Desincronización de la visibilidad de operaciones de escritura entre núcleos debido al store buffer.
  • Memory fences (barreras) — Instrucciones que sincronizan por fuerza el orden de operaciones de memoria entre hilos.
  • Atomic operations — Acciones read-modify-write indivisibles, implementadas vía LOCK y que encapsulan barreras.
  • LOCK + ANDB — Una forma funcional de implementar una operación atómica bitwise ausente en la biblioteca estándar de Go.

— Editorial Team

Advertisement 728x90

Leer después