Demystifying Rust - Memory Ordering

Migo·2023년 9월 3일

Demystifying Rust

목록 보기
3/4
post-thumbnail

일전의 포스트에서 Happens-before relationship에 대해 다룬 바가 있다. 오늘의 포스트는 해당 내용에 대한 기본적인 이해가 필요하니, 아래 포스트의 내용을 먼저 이해하고 읽어보길 권장한다.

https://velog.io/@migorithm/Demystifying-Rust-Happens-before-relationship

Problem - memory reordering

동시성 프로그래밍에서 가장 크게 이슈가 되는 문제를 하나만 꼽자면, Shared Resource에 대한 접근이다.

다시한번, 러스트에서 그런 race condition에 대한 문제는 언어 철학적인 레벨에서 Ownership, lifetime등의 개념 도입을 통해 대부분 해결했다.

그러나, memory reordering 문제는 컴파일시 프로세서가 최적화 단계에서 발생하는 문제이며, 이는 여전히 Shared resource에 대한 접근시 문제를 일으킬 수 있다. 가령 아래와 같은 코드가 있다고 생각해보자.

use std::{
    sync::atomic::{AtomicBool, AtomicU64, Ordering::Relaxed},
    thread,
};
static DATA: AtomicU64 = AtomicU64::new(0);
static READY: AtomicBool = AtomicBool::new(false);

fn main() {
    thread::spawn(|| {
        DATA.store(123, Relaxed);
        READY.store(true, Relaxed); 
    });
    while !READY.load(Relaxed) {
    
    }

    if DATA.load(Relaxed) == 0 && READY.load(Relaxed) {
        println!("Right");
    };
}

자, while !READY.load(Relaxed) {...}에 도달한 시점에, DATA의 값은 항상 123인가?

정답은, 이전 포스트에서 밝힌 바와 같이, "아니다"이다.

그것은 다른 thread사이happens-before relationship이 형성 되어있지 않기 때문이다. 즉, 하나의

thread내에서 컴파일러와 프로세서는 아래와 같은 최적화를 시전할 수 있다.

DATAREADY는 서로 영향을 주고 받지 않으니, 뭐가 되었든 빠른 것 부터 처리하면 되겠군...

따라서 메인 thread에서 READY의 값을 평가하는 시점에, 그 값은 true가 될 수 있고, 프린트로 찍히는 값은 0이 될 수 있다는 메커니즘이다.


아마도 위의 사실이 너무나도 비직관적이거나, 혹은 그저 확인을 해보고 싶은 독자가 있을 수 있겠다. 그리고 혹시 내가 사용하는 컴퓨터의 processor가 x86-64과 같은 Strongly Ordered를 보장하는 기반이라면, 위의 문제는 재현되지 않는다. 다시말해, 프로세서의 종류의 따라, 해당 이슈가 "저절로 해결되는" 케이스가 있다. (물론 퍼포먼스적 제약은 불가피하다)

Solution - Release & Acquire

shared resource에 접근하는 실행 라인들 사이에서 happens-before relationship을 형성하기 위해서는 아래 두 enum을 사용한다.

  • std::atomic::Ordering::Acquire
  • std::atomic::Ordering::Release
fn main() {
    thread::spawn(|| {
        DATA.store(123, Relaxed);
        READY.store(true, Release); 
    });
    while !READY.load(Relaxed) {
       
    }

    if READY.load(Acquire) && DATA.load(Relaxed) == 0 {
        println!("Right");
    };
}

위 Memory Ordering이 보장하는 것은 다음과 같다.

Release 사용 이전의 모든 기록들은, Acquire 사용 이후 보여지게된다.


Mutex - Locking

ReleaseAcquire는 우리가 자주 쓰는 Mutex가 어떻게 Atomic operation을 제공하는가에 대한 메커니즘을 제공한다. Lock을 시도할때, Mutex는 guard를 통해 해당 객체가 Unlock되어있는 지 Acquire를 통해 확인을 하고,

Unlock시, Release를 통해 프로세서에게 Memory ordering에 대한 정보를 줌으로써 happens-before relationship을 확립한다.

profile
Dude with existential crisis

0개의 댓글