이 글에서는 메모리 배리어와 멀티코어 스레드 프로그래밍에 대해서 설명합니다.
- 메모리 배리어 memory barrier
- 스레드 환경에서 경쟁 상태
- 뮤텍스를 이용한 경쟁 상태 문제 해결
- 메모리 배리어를 통한 경쟁 상태 문제 해결
메모리 배리어 memory barrier
메모리 배리어는 스레드 환경에서 사용됩니다. 멀티코어 CPU에서는 여러 개의 스레드가 동시에 실행될 수 있으므로 각 스레드는 자신이 사용하는 변수의 값을 다른 스레드와 동기화해야합니다. 예를 들어, 한 스레드에서 변수를 변경하고 다른 스레드에서 해당 변수의 값을 읽을 때 메모리 배리어를 사용하여 각 스레드가 최신 값을 사용하도록 보장해야합니다.
일반적으로 CPU 아키텍처에서는 두 종류의 메모리 배리어가 있습니다.
- Load Barrier (Read Barrier)
- Store Barrier (Write Barrier)
Load Barrier는 메모리에서 데이터를 읽기 전에 이전에 실행된 모든 명령어가 완료될 때까지 기다리도록합니다. Store Barrier는 메모리에 데이터를 쓰기 전에 이전에 실행된 모든 명령어가 완료될 때까지 기다리도록합니다.
아래는 x86 아키텍처에서 memory barrier를 만드는 예제입니다.
static inline void memory_barrier(void) { asm volatile("mfence":::"memory"); }
메모리 배리어는 메모리 오더링을 보장하는 데 사용됩니다. 메모리 오더링이란 명령어가 실행되는 순서가 예상과 일치하는 것을 보장하는 것입니다. 메모리 배리어를 사용하면 명령어의 실행 순서를 강제로 지정할 수 있으므로, 이를 통해 메모리 오더링을 유지할 수 있습니다.
따라서, 스레드 환경에서 공유 변수의 값을 동기화하고 메모리 오더링을 보장하기 위해서는 메모리 배리어를 사용해야 합니다.
하지만 일반적인 경우의 대부분은 메모리 배리어보다는 뮤텍스를 통해서 처리하는 것이 더 이해하기 쉽습니다.
스레드 환경에서 경쟁 상태
멀티 스레드 환경에서는 하나의 변수에 대한 접근이 동시에 일어날 수 있습니다. 따라서 스레드 두 개가 동시에 변수를 처리하면서 서로의 결과를 덮어쓰거나 예상치 못한 결과가 발생할 수 있습니다. 이러한 문제를 경쟁 상태 (Race Condition)이라고 합니다.
아래는 경쟁 상태가 발생하는 예제입니다.
#include <stdio.h> #include <pthread.h> int count = 0; void *thread_func(void *arg) { int i; for (i = 0; i < 1000000; i++) { count++; } return NULL; } int main() { pthread_t t1, t2; pthread_create(&t1, NULL, thread_func, NULL); pthread_create(&t2, NULL, thread_func, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("count = %d\n", count); return 0; }
위 예제는 pthread를 사용하여 두 개의 스레드를 생성하고, 각각 count 변수에 100만번씩 증가시키는 thread_func 함수를 실행합니다. main 함수에서는 두 스레드가 종료될 때까지 기다린 후 count 변수의 값을 출력합니다.
이 예제에서는 두 스레드가 count 변수에 대해 동시에 쓰기 연산을 수행하므로 경쟁 상태가 발생합니다. 이 때문에 count 변수의 값이 예상과 다르게 출력될 수 있습니다.
경쟁 상태를 해결하기 위해서는 스레드 간의 접근을 동기화해야 합니다. 일반적으로 동기화 기법으로 뮤텍스 (Mutex), 세마포어 (Semaphore), 조건 변수 (Condition Variable) 등이 사용됩니다.
뮤텍스를 이용한 경쟁 상태 문제 해결
예를 들어, 뮤텍스를 사용하여 count 변수의 접근을 동기화하는 코드는 다음과 같습니다.
#include <stdio.h> #include <pthread.h> int count = 0; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; void *thread_func(void *arg) { int i; for (i = 0; i < 1000000; i++) { pthread_mutex_lock(&mutex); count++; pthread_mutex_unlock(&mutex); } return NULL; } int main() { pthread_t t1, t2; pthread_create(&t1, NULL, thread_func, NULL); pthread_create(&t2, NULL, thread_func, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("count = %d\n", count); return 0; }
메모리 배리어를 통한 경쟁 상태 문제 해결
메모리 배리어는 스레드에서 메모리에 접근할 때 발생하는 쓰기 버퍼 플러시나 캐시 라인 무효화 등의 동작을 명시적으로 지정할 수 있도록 해주는 명령어입니다.
C 언어에서는 다음과 같은 형태로 메모리 배리어를 사용할 수 있습니다.
#include <stdio.h> #include <pthread.h> #include <stdatomic.h> int count = 0; void *thread_func(void *arg) { int i; for (i = 0; i < 1000000; i++) { atomic_fetch_add(&count, 1); __sync_synchronize(); } return NULL; } int main() { pthread_t t1, t2; pthread_create(&t1, NULL, thread_func, NULL); pthread_create(&t2, NULL, thread_func, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); printf("count = %d\n", count); return 0; }
위 코드에서는 atomic_fetch_add 함수를 사용하여 count 변수에 접근하고, __sync_synchronize 함수를 사용하여 메모리 배리어를 생성합니다. 이렇게 하면 스레드 간의 경쟁 상태를 방지할 수 있습니다.
하지만 메모리 배리어는 상황에 따라서 예상치 못한 결과를 낼 수 있으므로, 메모리 배리어를 사용할 때에는 주의해야 합니다. 일반적으로는 뮤텍스나 다른 동기화 기법을 사용하는 것이 더 안전한 방법입니다.