맨위로가기

메모리 배리어

"오늘의AI위키"는 AI 기술로 일관성 있고 체계적인 최신 지식을 제공하는 혁신 플랫폼입니다.
"오늘의AI위키"의 AI를 통해 더욱 풍부하고 폭넓은 지식 경험을 누리세요.

1. 개요

메모리 배리어는 멀티 프로세서 시스템에서 여러 스레드가 공유 메모리에 접근할 때 발생하는, 메모리 연산의 순서 문제를 해결하기 위한 기술이다. 컴파일러나 CPU가 메모리 연산의 순서를 임의로 변경하여 예상치 못한 결과를 초래하는 것을 방지하기 위해 사용되며, x86, x86-64, PowerPC 등의 아키텍처에서 하드웨어적으로 지원된다. 메모리 배리어는 하드웨어뿐 아니라 컴파일러의 명령어 재정렬에도 영향을 미치며, C/C++, Java, C# 등 다양한 프로그래밍 언어에서 `volatile` 키워드 또는 원자적 연산을 통해 메모리 배리어를 지원한다.

더 읽어볼만한 페이지

  • 일관성 모델 - 원자성
    원자성은 분자를 구성하는 원자 수로, 분자는 원자성에 따라 단원자, 이원자, 삼원자 분자 등으로 나뉘며, 금속이나 탄소는 원자성이 2로 간주되고 단원자 분자의 원자성은 분자량을 원자량으로 나누어 계산한다.
  • 일관성 모델 - 캐시 일관성
    캐시 일관성은 다중 프로세서 시스템에서 공유 메모리 데이터의 일관성을 유지하기 위해 읽기 및 쓰기 동작을 정의하며, 스누핑, 디렉터리 기반 방식 등의 메커니즘과 다양한 모델 및 프로토콜이 사용된다.
  • 명령어 처리 - 멀티스레딩
    멀티스레딩은 프로세스 내에서 여러 스레드를 동시 실행하여 처리 능력을 향상시키는 기술로, 응답성 향상과 자원 공유 등의 장점이 있지만, 자원 간섭과 소프트웨어 복잡성 증가 등의 단점도 존재하며, 다양한 모델과 구현 방식, 스레드 스케줄러, 가상 머신 활성화 가능성 등을 고려해야 한다.
  • 명령어 처리 - 마이크로아키텍처
    마이크로아키텍처는 명령어 집합 아키텍처를 구현하는 프로세서의 구성 요소, 상호 연결, 작동 방식을 포괄하는 개념으로, 동일 ISA에서 반도체 기술 발전과 새로운 구조 및 회로를 통해 성능 향상을 가능하게 한다.
  • 컴퓨터 메모리 - 플래시 메모리
    플래시 메모리는 전기적으로 데이터의 쓰기 및 삭제가 가능한 비휘발성 메모리 기술로, 마스오카 후지오 박사가 발명하여 카메라 플래시와 유사한 소거 방식으로 인해 명명되었으며, NOR형과 NAND형으로 나뉘어 각기 다른 분야에 적용된다.
  • 컴퓨터 메모리 - 메모리 계층 구조
    메모리 계층 구조는 CPU 데이터 접근 속도 향상을 위해 레지스터, 캐시, RAM, 보조 기억 장치 등으로 구성되며, 속도, 용량, 비용이 다른 계층들을 통해 효율적인 메모리 관리를 가능하게 한다.
메모리 배리어
개요
종류컴퓨터 동기화 명령어
다른 이름메모리 펜스 (memory fence)
펜스 명령어 (fence instruction)
용도컴파일러CPU가 메모리 접근 순서를 재배열하는 것을 방지
목표다중 스레드 환경에서 예측 불가능한 결과를 방지
상세 설명
역할특정 종류의 최적화를 제한하고, 메모리 모델에 따른 순서 제약 조건을 강제
적용상호 배제 데이터 구조
장치 드라이버
필요성특정 컴파일러와 CPU가 메모리 접근 순서를 재배열하여 예상치 못한 결과가 발생하는 것을 막기 위해 필요
구현 방식
일반적인 구현특수 기계어 명령어를 사용
컴파일러 장벽과의 차이점컴파일러 장벽은 컴파일러의 코드 재배치만 막음
메모리 배리어는 CPU의 재배치와 컴파일러의 재배치를 모두 막음
예시
C++11의 메모리 배리어std::atomic_thread_fence 함수 사용
Java의 메모리 배리어volatile 키워드를 사용하여 변수를 선언

2. 예제

멀티스레딩 환경에서 여러 스레드가 공유 메모리에 접근할 때, 컴파일러CPU는 성능 최적화를 위해 코드에 명시된 명령어의 실행 순서를 변경할 수 있다. 이러한 명령어 재배치(Out-of-order execution)는 단일 스레드 환경에서는 문제가 되지 않지만, 여러 스레드가 상호작용하는 경우에는 프로그래머가 의도하지 않은 결과를 초래할 수 있다.

예를 들어, 한 스레드가 특정 데이터를 준비하고 플래그 변수(flag)를 설정하여 다른 스레드에게 준비 완료를 알리는 경우를 생각해 볼 수 있다. 만약 명령어 재배치가 발생하여 플래그 변수가 데이터 준비보다 먼저 설정된다면, 다른 스레드는 데이터가 준비되지 않은 상태에서 작업을 시작하여 오류를 일으킬 수 있다.

메모리 배리어는 이러한 명령어 재배치를 제한하여 특정 지점 이전의 메모리 연산이 이후의 메모리 연산보다 먼저 완료되도록 보장하는 역할을 한다. 이를 통해 멀티스레드 환경에서도 프로그램이 예측 가능하게 동작하도록 도울 수 있다. 특히 멀티 코어 프로세서 환경이나 메모리 맵 I/O를 사용하는 경우 메모리 배리어의 중요성은 더욱 커진다. 구체적인 코드 예시와 실제 프로그래밍 패턴에서의 활용은 하위 섹션에서 더 자세히 살펴볼 수 있다.

2. 1. 코드 예제

다음 코드는 메모리 배리어가 없는 경우 의도하지 않은 동작을 실행할 수 있음을 보여준다:

스레드 1스레드 2



스레드 1의 `print y;`가 의도대로 10을 출력하려면, 그 전에 `x == 0` 조건 검사가 먼저 완료되어야 한다. 하지만 스레드 2에서 `y = 10;`과 `x = 1;`은 서로 의존성이 없기 때문에, 컴파일러CPU가 최적화를 위해 이 두 연산의 순서를 바꿀 수 있다. 마찬가지로, 스레드 1에서도 `print y;`에서 변수 y 값을 읽는 동작이 `while(x == 0)`에서 x 값을 읽는 동작보다 먼저 일어날 수 있다. 각 스레드의 해당 위치에 메모리 배리어를 삽입하면, 컴파일러나 CPU가 코드에 명시된 순서대로 명령을 실행하도록 강제하여 원하는 결과를 얻을 수 있다.

프로그램이 단일 CPU 환경에서 실행될 때는 하드웨어가 모든 메모리 연산이 프로그래머가 지정한 순서(프로그램 순서)대로 수행되는 것처럼 보이도록 처리하므로 메모리 배리어가 필요하지 않다. 그러나 메모리가 멀티 프로세서 시스템의 다른 CPU나 메모리 맵 I/O 주변 장치 등 여러 장치와 공유될 때는, 순서 없이 메모리에 접근하는 것이 프로그램 동작에 예기치 않은 영향을 미칠 수 있다. 예를 들어, 한 CPU가 메모리를 변경하는 순서가 프로그램 코드 순서와 다르게 다른 CPU에게 관찰될 수 있다.

이러한 문제는 단일 프로세스 내에서 실행되는 여러 개의 소프트웨어 스레드(예: pthread)가 멀티 코어 프로세서에서 동시적으로 실행될 때 발생할 수 있다. 서로 다른 프로세스는 각자의 독립된 메모리 공간을 가지므로 이 논의에 해당하지 않는다.

다음은 멀티 코어 프로세서에서 실행되는 다중 스레드 프로그램에서 순서 없는 실행이 어떻게 문제를 일으킬 수 있는지 보여주는 예시다. 초기 상태에서 메모리 위치 `x`와 `f`의 값은 모두 `0`이다.

스레드 #1 (코어 #1 실행):



while (f == 0); // f가 0이 아닐 때까지 대기

// 여기에 메모리 펜스 필요

print x; // x 값 출력



스레드 #2 (코어 #2 실행):



x = 42;

// 여기에 메모리 펜스 필요

f = 1;



이 코드에서 스레드 #1이 항상 "42"를 출력할 것으로 기대할 수 있다. 하지만 메모리 배리어가 없다면, 스레드 #2의 저장 연산 순서가 바뀌어 `f`가 `x` ''전에'' 업데이트될 수 있다. 이 경우 스레드 #1은 `f`가 1이 된 것을 보고 루프를 빠져나왔지만, 아직 `x`는 42로 업데이트되지 않았을 수 있으므로 "0"을 출력할 수 있다. 마찬가지로, 스레드 #1의 읽기 연산 순서가 바뀌어 `f` 값을 확인하기 ''전에'' `x` 값을 먼저 읽을 수도 있다. 이러한 예기치 않은 동작을 막으려면, 스레드 #2가 `f`에 값을 할당하기 전과 스레드 #1이 `x`에 접근하기 전에 각각 메모리 배리어를 삽입해야 한다.

PowerPC 프로세서의 경우, `eieio` (Enforce In-order Execution of I/O) 명령어는 메모리 펜스 역할을 한다. 이 명령어는 이전에 시작된 모든 로드 또는 저장 연산이 프로세서가 주 메모리에 접근하기 전에 완료되도록 보장한다.[1][2]

또 다른 예시는 드라이버가 다음과 같은 작업을 수행할 때 발생할 수 있다:



// 하드웨어 모듈을 위한 데이터 준비

데이터_버퍼 = 준비된_데이터;

// 여기에 메모리 펜스 필요

// 하드웨어 모듈이 데이터를 처리하도록 트리거

하드웨어_제어_레지스터 = 시작_신호;



만약 프로세서가 저장 연산 순서를 바꾼다면, 데이터가 메모리에 완전히 준비되기 전에 하드웨어 모듈이 트리거될 수 있어 오류가 발생할 수 있다.

실제 프로그래밍에서 발생하는 더 복잡한 예시로는 이중 점검 잠금(Double-checked locking) 패턴이 있다.

2. 2. 이중 점검 잠금

실제 프로그래밍 환경에서 메모리 배리어가 필요한 중요하고 비자명한(non-trivial) 예시로 이중 점검 잠금(Double-checked locking) 패턴이 있다.

3. 하드웨어 지원

메모리 배리어는 특정 CPU 아키텍처의 메모리 모델 정의에 포함되는 저수준 기능이다. 명령어 집합처럼 메모리 모델도 아키텍처마다 다르기 때문에, 메모리 배리어의 동작 방식 역시 아키텍처에 따라 달라진다. 따라서 특정 하드웨어에서 메모리 배리어를 올바르게 사용하려면 해당 아키텍처의 매뉴얼을 참조하는 것이 중요하다.

단일 CPU 환경에서는 하드웨어가 프로그램에 작성된 순서대로 메모리 연산이 실행되는 것처럼 보장해주므로 메모리 배리어가 필요하지 않다. 하지만 여러 CPU가 메모리를 공유하는 멀티 코어 프로세서 시스템이나, 메모리 맵 I/O처럼 CPU 외 다른 장치와 메모리를 공유하는 환경에서는 문제가 발생할 수 있다. 한 CPU가 메모리에 값을 쓰는 순서와 다른 CPU가 그 값을 읽는 순서가 프로그램 코드 순서와 다르게 관측될 수 있기 때문이다.[21] 예를 들어, 한 스레드가 데이터 준비를 마치고 완료 플래그를 설정하는 경우, 메모리 배리어가 없다면 다른 스레드에서는 플래그가 먼저 설정되고 데이터는 아직 준비되지 않은 상태로 보일 수 있다.

다양한 CPU 아키텍처는 이러한 메모리 순서 문제를 해결하기 위해 각기 다른 메모리 배리어 명령어를 제공한다.


  • PowerPC: `eieio` (Enforce In-order Execution of I/O) 명령어는 일종의 메모리 펜스로 작동한다. 이 명령 이전에 시작된 모든 로드 또는 스토어 연산이, 이 명령 이후의 연산이 주 메모리에 접근하기 전에 완료되도록 보장한다.[1][2][5]
  • x86 및 x86-64: 이 아키텍처들 역시 메모리 연산 순서를 보장하기 위한 명령어를 제공한다. 자세한 내용은 하위 섹션에서 다룬다.


일부 아키텍처는 모든 종류의 메모리 접근 순서를 보장하는 풀 펜스(Full Fence) 명령어 하나만 제공하는 경우도 있다. 반면, 다른 아키텍처에서는 읽기 연산과 쓰기 연산의 순서를 분리하여 제어하는 acquirerelease 방식의 배리어를 제공하기도 한다.[6] Acquire 배리어는 특정 메모리 값을 읽은 후에 다른 연산을 시작하기 전에 사용되고, Release 배리어는 특정 메모리 값을 쓰기 전에 이전 연산들이 모두 완료되었음을 보장하는 데 사용된다. 또한, 주 메모리와 I/O 메모리 접근을 구분하여 더 세분화된 배리어 명령어를 제공하는 아키텍처도 있다. 이러한 다양한 종류의 메모리 배리어 명령어는 성능에 미치는 영향(비용)이 각기 다를 수 있다는 점에 유의해야 한다.

메모리 배리어는 동기화 프리미티브를 구현하는 데 핵심적인 역할을 하며, 참조 카운트나 플래그 관리, 스핀 락, 뮤텍스와 같은 동기화 기법의 기반이 된다.

3. 1. x86 및 x86-64

x86과 x86-64 CPU에서는 서로 종속성이 없는 메모리 연산의 순서가 보장되지 않으며, 이를 보장하기 위한 SFENCE, LFENCE, MFENCE 명령어가 존재한다.[21]

4. 저수준 아키텍처용 프리미티브

메모리 배리어는 아키텍처의 메모리 모델 정의의 일부를 구성하는 저수준 프리미티브이다. 명령어 집합과 마찬가지로 메모리 모델은 아키텍처마다 다르므로, 그 동작을 일반화하여 설명하기는 어렵다. 일반적으로 메모리 배리어를 올바르게 사용하려면 프로그래밍 대상 하드웨어의 아키텍처 매뉴얼을 참조해야 한다. 아래에서는 몇 가지 실제 메모리 배리어의 예를 소개한다.

일부 아키텍처에서는 "풀 펜스(full fence)"라고 불리는 한 종류의 메모리 배리어 명령만을 제공한다. 풀 펜스 명령은 해당 명령 이전의 모든 로드 및 스토어 명령이, 펜스 이후의 로드 및 스토어 명령보다 먼저 완료됨을 보장한다. PowerPC의 `eioio` 명령이 이러한 풀 펜스의 한 예이다.[5]

다른 아키텍처에서는 "acquire" 명령과 "release" 명령으로 메모리 배리어를 구성한다. 이는 읽기-후-쓰기(read-after-write) 연산의 가시성에 관한 것으로, 읽는 측(acquire)과 쓰는 측(release)이 각자의 관점에서 명령을 사용한다.[6] 구체적으로, acquire 측에서는 순서 보장이 필요한 메모리 연산 후에 메모리 배리어를 배치한다. 이는 해당 메모리 연산의 결과를 다른 CPU나 스레드에 보이고, 그 결과에 의존하는 후속 처리를 시작하기 위함이다. 반대로 release 측에서는 메모리 배리어 후에 순서 보장이 필요한 메모리 연산을 실행한다. 이는 배리어 이전까지의 모든 처리가 완료되었음을 알리는 메모리 연산을 수행하기 위함이다.

몇몇 아키텍처에서는 주 기억 장치와 I/O 메모리 연산의 조합에 따라 여러 종류의 메모리 배리어 명령을 제공하기도 한다. 여러 종류의 메모리 배리어 명령이 존재하는 아키텍처에서는 각 명령을 실행하는 데 드는 비용에 큰 차이가 있을 수 있다는 점에 유의해야 한다.

순서가 중요한 메모리 내용이 단일 정수 값처럼 하나의 메모리 연산 명령으로 조작될 수 있는 경우, 이 연산과 메모리 배리어를 조합하면 락 프리미티브보다 더 가벼운 동기화 메커니즘을 구현할 수 있다. 참조 카운트나 메모리 상의 플래그가 대표적인 예시이며, 스핀 락이나 뮤텍스와 같은 경량 락 프리미티브에서도 락 변수를 구현하는 수단으로 종종 사용된다. 이처럼 메모리 연산 명령과 메모리 배리어의 조합에 대한 수요가 많기 때문에, 이를 API로 제공하는 경우가 있다. 예를 들어 리눅스 커널에서는 매크로 형태인 `atomic_''operation''_''visibility''()`를 제공한다. 여기서 ''operation''에는 메모리 연산 명령을, ''visibility''에는 가시성 수준을 지정하여, 이러한 조합을 C 언어에서 쉽게 사용할 수 있도록 지원한다.

5. 멀티스레드 프로그래밍과 메모리 가시성

멀티스레드 프로그래밍 환경에서는 여러 스레드가 메모리를 공유하며 동시에 작업을 수행한다. 이때 각 스레드가 메모리에 접근하는 순서가 프로그래머가 의도한 대로 보장되지 않으면 예상치 못한 문제가 발생할 수 있다. 이를 메모리 가시성 문제라고 한다. 예를 들어, 한 스레드가 변수 `y`에 값을 쓰고 변수 `x`를 1로 설정하고, 다른 스레드가 `x`가 1이 될 때까지 기다렸다가 `y`의 값을 읽는 상황을 생각해보자. 컴파일러나 CPU는 최적화를 위해 코드의 실행 순서를 변경할 수 있으므로, 스레드 2에서 `x = 1;`이 `y = 10;`보다 먼저 실행되거나, 스레드 1에서 `y`를 읽는 동작이 `x`를 확인하는 동작보다 먼저 일어날 수 있다. 이런 경우 스레드 1은 엉뚱한 `y` 값을 읽게 될 수 있다. 메모리 배리어는 이러한 명령어 순서 변경을 막아 메모리 연산 순서를 강제함으로써 프로그램이 의도한 대로 동작하도록 보장하는 역할을 한다.

단일 CPU 환경에서는 하드웨어가 메모리 연산 순서를 보장해주므로 일반적으로 메모리 배리어가 필요하지 않다. 하지만 여러 CPU가 메모리를 공유하는 멀티 코어 프로세서 환경이나, 메모리 맵 I/O처럼 여러 장치가 메모리를 공유하는 경우에는 각 장치나 CPU가 메모리 변경 사항을 인지하는 순서가 달라 문제가 발생할 수 있다.

이러한 문제를 해결하기 위해, 대부분의 멀티스레드 프로그램은 POSIX 스레드(Pthreads), Windows API, 자바, .NET과 같은 프로그래밍 환경이나 API에서 제공하는 동기화 기본 요소(primitive)를 사용한다. 대표적인 동기화 기본 요소로는 뮤텍스나 세마포어가 있으며, 이들은 여러 스레드가 공유 자원에 안전하게 접근하도록 돕는다.[7][8][9] 이러한 동기화 기본 요소들은 내부적으로 필요한 메모리 배리어를 포함하여 구현되는 경우가 많아, 개발자가 명시적으로 메모리 배리어를 사용할 필요성을 줄여준다.[10][11] 따라서 대부분의 경우 동기화 기본 요소를 사용하는 것이 더 간결하고 안전한 방법이다.[12]

하지만 각 프로그래밍 환경이나 API는 자체적인 메모리 모델을 가지고 메모리 가시성을 정의하므로, 사용하는 환경의 메모리 모델을 이해하는 것이 중요하다. 프로그래밍 환경의 메모리 모델은 하드웨어 수준의 메모리 모델과는 추상화 수준이 다르며, 때로는 특정 플랫폼의 구현이 표준 명세보다 더 강력한 메모리 가시성을 제공할 수도 있다. 따라서 특정 구현에 의존하기보다는 해당 환경의 공식적인 메모리 모델 사양을 기준으로 프로그래밍하는 것이 이식성을 높이는 데 도움이 된다. 이중 점검 잠금 패턴은 메모리 가시성 문제와 관련된 더 복잡한 예시 중 하나이다.

6. 아웃 오브 오더 실행과 컴파일러 재정렬 최적화

현대의 CPU는 성능 향상을 위해 명령어 처리 순서를 프로그램 코드에 명시된 순서와 다르게 변경하는 아웃 오브 오더 실행 기법을 사용한다. 또한, 컴파일러 역시 프로그램 최적화 과정에서 코드의 실행 순서를 재배치할 수 있다. 이러한 명령어 재정렬은 단일 스레드 환경에서는 프로그램의 최종 결과에 영향을 주지 않도록 설계되지만, 여러 스레드가 메모리를 공유하는 멀티 코어 프로세서 환경에서는 예기치 않은 문제를 일으킬 수 있다.[1][2]

예를 들어, 한 스레드가 공유 변수에 값을 쓰고 플래그를 설정하는 동안 다른 스레드가 플래그를 확인하고 해당 공유 변수 값을 읽는 경우, 명령어 재정렬로 인해 플래그 설정 전에 값을 쓰거나, 플래그 확인 전에 값을 읽는 상황이 발생할 수 있다. 이는 데이터 일관성을 깨뜨리고 프로그램 오작동으로 이어질 수 있으며, 메모리 맵 I/O를 사용하는 경우에도 유사한 문제가 발생할 수 있다.

이러한 하드웨어 수준의 명령어 재정렬 문제를 해결하기 위해 메모리 배리어(Memory Barrier) 또는 메모리 펜스(Memory Fence) 명령어가 사용된다. 메모리 배리어는 특정 지점에서 이전의 메모리 연산(읽기 또는 쓰기)이 모두 완료된 후에 다음 연산이 실행되도록 강제한다.[13] 예를 들어 PowerPC 프로세서의 `eieio` 명령어가 이러한 역할을 수행한다.[1][2]

하지만 메모리 배리어 명령어는 하드웨어 수준의 재정렬 효과만을 다룬다. 컴파일러 역시 최적화 과정에서 명령어를 재정렬할 수 있으며, 이는 하드웨어 재정렬과 유사한 문제를 일으킬 수 있다. 따라서 여러 스레드에서 공유되는 데이터에 대해서는 컴파일러의 재정렬 최적화를 억제하기 위한 별도의 조치가 필요하다.[14] CC++의 `volatile` 키워드가 컴파일러 최적화를 일부 제한하지만, 멀티프로세서 환경에서의 모든 재정렬 문제를 해결하기에는 부족하며[3][4], 최신 언어 표준에서는 보다 명확한 원자적 연산 및 메모리 배리어 기능을 제공하는 추세이다.[20] 이중 점검 잠금(Double-checked locking)과 같은 프로그래밍 패턴에서도 이러한 재정렬 문제가 발생할 수 있어 주의가 필요하다.

6. 1. C/C++ `volatile` 키워드

CC++에서 'volatile' 키워드는 본래 메모리 맵 I/O에 직접 접근하는 프로그램을 위해 고안되었다. 메모리 맵 I/O는 일반적으로 소스 코드에 명시된 읽기와 쓰기가 컴파일러에 의해 생략되거나 순서가 바뀌지 않고 정확히 실행되어야 한다. 컴파일러가 프로그램 최적화 과정에서 읽기/쓰기를 생략하거나 재정렬하면, 프로그램과 메모리 맵 I/O 장치 간의 통신에 문제가 발생할 수 있다. C/C++ 컴파일러는 'volatile'로 선언된 메모리 위치에 대한 읽기나 쓰기를 임의로 생략할 수 없으며, 동일한 'volatile' 변수에 대한 다른 접근과 순서를 바꾸지 않는다.

그러나 'volatile' 키워드는 캐시 일관성을 강제하기 위한 메모리 배리어를 보장하지 않는다. 따라서 'volatile'만으로는 여러 스레드가 공유하는 변수를 사용한 스레드 간 통신에 충분하지 않다.[3]

C11C++11 이전의 C/C++ 표준은 멀티스레딩이나 멀티프로세서를 명시적으로 다루지 않았기 때문에[4], 'volatile'의 실제 동작과 유용성은 컴파일러와 하드웨어에 따라 달라질 수 있었다. 'volatile'은 해당 변수에 대한 접근 순서는 보장하지만, 컴파일러가 코드를 생성하거나 CPU가 아웃 오브 오더 실행을 통해 'volatile' 접근과 'volatile'이 아닌 접근의 순서를 재정렬할 수 있다. 이러한 점 때문에 스레드 간의 동기화를 위한 플래그나 뮤텍스로 사용하기에는 한계가 있다.[14] 즉, 'volatile'은 컴파일러 수준의 최적화(명령어 재정렬 및 생략)만을 제한하며, 멀티프로세서 환경에서 발생할 수 있는 하드웨어 수준의 메모리 재정렬 문제까지 해결하지는 못한다.

Microsoft Visual C++ 컴파일러는 표준 C/C++의 'volatile'과 달리, 독자적인 확장을 통해 제한적인 메모리 배리어 기능을 제공하기도 했다.[19] 그러나 Visual C++에서도 x86/x64 아키텍처를 위한 배리어 내장 함수는 더 이상 권장되지 않으며, C++11 표준 라이브러리에 추가된 'std::atomic_thread_fence'나 'std::atomic<T>'와 같은 원자적 연산 관련 기능을 사용하는 것이 권장된다.[20]

6. 2. C11 및 C++11 이후의 표준

C11 및 C++11 이전의 C 및 C++ 표준은 여러 스레드나 여러 프로세서를 직접적으로 다루지 않았다.[4] 이 시기의 'volatile' 키워드는 주로 메모리 맵 I/O 접근 시 컴파일러가 프로그램 최적화 과정에서 메모리 접근 명령의 순서를 바꾸거나 생략하는 것을 막기 위해 사용되었다.[3] 즉, 소스 코드에 명시된 순서대로 읽기와 쓰기가 일어나도록 보장하는 역할이었다.

그러나 'volatile'은 컴파일러 수준의 최적화만을 제어할 뿐, 하드웨어 수준에서 발생하는 명령어 재정렬이나 캐시 일관성 문제까지 해결하지는 못했다.[3][14] 따라서 멀티스레드 환경에서 스레드 간 통신을 위해 'volatile' 변수만 사용하는 것은 충분하지 않으며, 예기치 않은 동작을 일으킬 수 있었다.[3]

다른 프로그래밍 언어에서는 이러한 문제를 해결하기 위한 기능을 제공하기도 한다. 예를 들어, Java 1.5 이후 버전과 C#에서는 'volatile' 키워드가 컴파일러 재정렬뿐만 아니라 하드웨어 수준의 재정렬까지 방지하는 메모리 배리어 역할을 수행하도록 정의되어 있다.[15][16][17] 또한, 이들 언어는 원자적 연산을 위한 표준 라이브러리(Java의 `java.util.concurrent.atomic` 패키지[15][16], C#의 `System.Threading.Interlocked` 클래스[18])를 제공한다. Microsoft Visual C++ 컴파일러 역시 표준 C/C++ 규격에는 없는 독자적인 확장 기능을 통해 'volatile'에 유사한 메모리 배리어 기능을 부여하기도 했다.[19]

이러한 배경 속에서, C++11 표준부터는 멀티스레딩 환경에서의 메모리 접근 순서를 명확하게 제어하기 위한 표준화된 방법을 도입했다. 대표적인 것이 'std::atomic_thread_fence' 함수와 'std::atomic' 템플릿 클래스이다.[20] 이들은 다양한 수준의 메모리 배리어 기능을 제공하여, 개발자가 명시적으로 메모리 접근 순서를 제어하고 데이터 경쟁 상태를 방지할 수 있도록 돕는다. 따라서 C++11 이후의 환경에서는 Visual C++의 비표준 내장 함수[19] 등 특정 컴파일러에 의존적인 방식보다는 표준 라이브러리의 'std::atomic_thread_fence'나 'std::atomic'를 사용하는 것이 권장된다.[20]

6. 3. Java 및 C#

Java는 버전 1.5(Java 5라고도 불린다)부터 새로운 메모리 모델을 채택했다. 이 모델에서는 `volatile` 키워드를 사용하면, 하드웨어 수준의 명령 재정렬과 컴파일러 최적화에 의한 재정렬 모두를 방지하는 것이 보장된다.[15][16] 일반적인 원자적 연산이 필요할 때는 `java.util.concurrent.atomic` 패키지에 포함된 클래스들을 사용한다.

C#의 `volatile` 키워드도 Java와 유사하게 하드웨어 및 컴파일러 수준의 재정렬을 방지하는 기능을 제공한다.[17] C#에서 일반적인 원자적 연산은 `System.Threading.Interlocked` 클래스를 통해 수행할 수 있다.[18]

이는 C나 C++ 표준 규격의 `volatile`과는 다른 점인데, 표준 C/C++의 `volatile`은 주로 컴파일러 최적화만을 억제하며 멀티프로세서 환경에서의 하드웨어 재정렬 문제까지는 해결하지 못한다.[14] 다만, Microsoft Visual C++ 컴파일러는 독자적인 확장 기능을 통해 `volatile`에 유사한 메모리 배리어 기능을 제공하기도 했다.[19] Visual C++는 과거 x86/x64 아키텍처에 한해 배리어용 내장 함수를 지원했지만, 현재는 권장되지 않으며 C++11 표준 라이브러리에 추가된 `std::atomic_thread_fence`나 `std::atomic` 사용이 권장된다.[20]

참조

[1] 서적 The PowerPC Architecture: A Specification for a New Family of RISC Processors Morgan Kaufmann Publishers 1993
[2] 서적 Optimizing PowerPC Code Addison-Wesley Publishing Company 1995
[3] 웹사이트 Why the 'Volatile' Type Class Should not Be Used https://www.kernel.o[...] 2023-04-13
[4] 컨퍼런스 Threads Cannot Be Implemented As a Library http://dl.acm.org/ci[...] Association for Computing Machinery 2005-06
[5] 서적 The PowerPC architecture: A SPECIFICATION FOR A NEW FAMILY OF RISC PROCESSORS Morgan Kaufmann PUblishers, Inc 1993
[6] 웹사이트 Acquire and Release Semantics - Windows drivers | Microsoft Learn https://learn.micros[...]
[7] 웹사이트 Interprocess Synchronization - Win32 apps | Microsoft Learn https://learn.micros[...]
[8] 웹사이트 Critical Section Objects - Win32 apps | Microsoft Learn https://learn.micros[...]
[9] 웹사이트 Mutex Class (System.Threading) | Microsoft Learn https://learn.micros[...]
[10] 웹사이트 Synchronization and Multiprocessor Issues - Win32 apps | Microsoft Learn https://learn.micros[...]
[11] 웹사이트 Semaphore (Java Platform SE 8 ) https://docs.oracle.[...]
[12] 웹사이트 Thread.MemoryBarrier Method (System.Threading) | Microsoft Learn https://learn.micros[...]
[13] 웹사이트 PowerPC storage model and AIX programming https://www.ibm.com/[...] 2021-01-10
[14] 웹사이트 POS03-C. volatile を同期用プリミティブとして使用しない https://www.jpcert.o[...]
[15] 웹사이트 Atomic Access (The Java™ Tutorials > Essential Java Classes > Concurrency) https://docs.oracle.[...]
[16] 웹사이트 Javaにおける同期(パート3):アトミック操作とデッドロック https://blogs.oracle[...]
[17] 웹사이트 volatile - C# Reference | Microsoft Learn https://learn.micros[...]
[18] 웹사이트 Interlocked Class (System.Threading) | Microsoft Learn https://learn.micros[...]
[19] 웹사이트 volatile (C++) | Microsoft Learn https://learn.micros[...]
[20] 웹사이트 _ReadWriteBarrier | Microsoft Learn https://learn.micros[...]
[21] 저널 Intel® 64 and IA-32 Architectures Software Developer’s Manual http://download.inte[...]



본 사이트는 AI가 위키백과와 뉴스 기사,정부 간행물,학술 논문등을 바탕으로 정보를 가공하여 제공하는 백과사전형 서비스입니다.
모든 문서는 AI에 의해 자동 생성되며, CC BY-SA 4.0 라이선스에 따라 이용할 수 있습니다.
하지만, 위키백과나 뉴스 기사 자체에 오류, 부정확한 정보, 또는 가짜 뉴스가 포함될 수 있으며, AI는 이러한 내용을 완벽하게 걸러내지 못할 수 있습니다.
따라서 제공되는 정보에 일부 오류나 편향이 있을 수 있으므로, 중요한 정보는 반드시 다른 출처를 통해 교차 검증하시기 바랍니다.

문의하기 : help@durumis.com