Workspace IndexAlgorithms › Day 36

Memory Models and Atomic Ordering — acquire/release/seq_cst TODO

Algorithms · Day 36 / 100 · C. Concurrency & Performance Engineering (Day 36-51)

Concept

As long as single-thread semantics are preserved, CPUs and compilers are free to reorder memory accesses, so what other threads see, and in what order, is governed by the memory model. A relaxed atomic operation only guarantees the atomicity of the operation itself, not its ordering relative to surrounding accesses. When a release store pairs with an acquire load that reads its value, a happens-before relationship is established: everything written before the release becomes visible to code after the acquire. seq_cst goes further and guarantees a single global order across all seq_cst operations, making it the most expensive. Go doesn't expose these ordering options directly — it describes its memory model in terms of the happens-before rules created by channels, mutexes, and sync/atomic.

Getting ordering wrong usually still passes on x86, and only shows up under a weaker memory model like ARM or under heavy load, which makes it an extremely hard-to-reproduce bug.

Code & Formula

# 메모리 모델과 원자성 순서 — release/acquire 페어링이 만드는 happens-before 관계를 흉내낸다.
# (Python 은 GIL 때문에 진짜 하드웨어 재배열은 안 보이지만, release-store -> acquire-load 짝짓기 패턴 자체는 동일하다.)

import threading

data = 0
ready = threading.Event()  # release/acquire 짝을 흉내내는 신호: set()=release, wait()=acquire
observations = []

def writer():
    global data
    data = 42                 # release 이전의 모든 쓰기는...
    ready.set()                # ...release. 이 시점 이후 acquire 한 쪽에는 반드시 42가 보여야 한다.

def reader():
    ready.wait()               # acquire: release 이전의 모든 쓰기가 happens-before 로 보장되어 보인다.
    observations.append(data)  # 만약 순서 보장이 없었다면(relaxed) 0을 볼 수도 있었다.

# seq_cst 라면 여기에 더해 "모든 스레드가 동의하는 단일 전역 순서"까지 보장하지만, 비용이 가장 크다.
runs_correct = 0
for _ in range(1000):
    data = 0
    ready.clear()
    t_r = threading.Thread(target=reader)
    t_w = threading.Thread(target=writer)
    t_r.start(); t_w.start()
    t_r.join(); t_w.join()
    if observations[-1] == 42:
        runs_correct += 1

print("total runs:", len(observations))
print("runs where acquire correctly saw release's write:", runs_correct)
print("release/acquire happens-before held every time:", runs_correct == len(observations))

Exercise

Implement the classic flag/data variable pattern once with relaxed and once with release/acquire ordering, run it millions of times on an ARM-family device, and count how often the ordering appears reversed.

Practical Connection

This is the correctness basis for lock-free queues and shared-state flags in high-performance server code — for Verex's in-memory order book shared across multiple goroutines, it's the standard for judging where atomic operations suffice and where a mutex becomes necessary.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

메모리 모델과 원자성 순서 TODO

Algorithms · Day 36 / 100 · C. 동시성·성능 엔지니어링 (Day 36–51)

acquire/release/seq_cst

개념

CPU와 컴파일러는 단일 스레드 의미가 보존되는 한 메모리 접근을 자유롭게 재배열하므로, 다른 스레드에 무엇이 어떤 순서로 보이는지는 메모리 모델이 규정한다. relaxed 원자 연산은 연산 자체의 원자성만 보장하고 주변 접근과의 순서는 보장하지 않는다. release 저장과 그 값을 읽은 acquire 로드가 짝을 이루면 release 이전의 모든 쓰기가 acquire 이후 코드에 보이는 happens-before 관계가 성립한다. seq_cst는 여기에 더해 모든 seq_cst 연산에 대한 단일 전역 순서를 보장하며 가장 비싸다. Go는 이런 순서 옵션을 직접 노출하지 않고 채널·뮤텍스·sync/atomic이 만드는 happens-before 규칙으로 메모리 모델을 기술한다.

순서 지정을 잘못해도 x86에서는 대개 통과하고 ARM처럼 약한 메모리 모델이나 고부하에서만 드러나서, 재현이 극도로 어려운 버그가 된다.

코드 · 수식

# 메모리 모델과 원자성 순서 — release/acquire 페어링이 만드는 happens-before 관계를 흉내낸다.
# (Python 은 GIL 때문에 진짜 하드웨어 재배열은 안 보이지만, release-store -> acquire-load 짝짓기 패턴 자체는 동일하다.)

import threading

data = 0
ready = threading.Event()  # release/acquire 짝을 흉내내는 신호: set()=release, wait()=acquire
observations = []

def writer():
    global data
    data = 42                 # release 이전의 모든 쓰기는...
    ready.set()                # ...release. 이 시점 이후 acquire 한 쪽에는 반드시 42가 보여야 한다.

def reader():
    ready.wait()               # acquire: release 이전의 모든 쓰기가 happens-before 로 보장되어 보인다.
    observations.append(data)  # 만약 순서 보장이 없었다면(relaxed) 0을 볼 수도 있었다.

# seq_cst 라면 여기에 더해 "모든 스레드가 동의하는 단일 전역 순서"까지 보장하지만, 비용이 가장 크다.
runs_correct = 0
for _ in range(1000):
    data = 0
    ready.clear()
    t_r = threading.Thread(target=reader)
    t_w = threading.Thread(target=writer)
    t_r.start(); t_w.start()
    t_r.join(); t_w.join()
    if observations[-1] == 42:
        runs_correct += 1

print("total runs:", len(observations))
print("runs where acquire correctly saw release's write:", runs_correct)
print("release/acquire happens-before held every time:", runs_correct == len(observations))

연습

flag/data 두 변수 패턴을 relaxed와 release/acquire로 각각 구현해 ARM 계열 기기에서 수백만 회 반복 실행하며 순서가 뒤집혀 보이는 경우를 세어보라.

실무 · Verex 연결

락 프리 큐나 공유 상태 플래그를 쓰는 고성능 서버 코드의 정확성 근거이며, Verex 인메모리 오더북을 여러 고루틴이 공유할 때 어디까지 원자 연산으로 되고 어디부터 뮤텍스가 필요한지 판단하는 기준이 된다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 35. [복습] 실행 계층 지도 한 장으로37. 락프리·wait-free, ABA 문제, 해저드 포인터·에포크 회수 →