본문 바로가기
Programming/C#

멀티스레딩에서 발생하는 메모리 재정렬 현상과 해결 방법

by Dev_카페인 2024. 7. 1.
반응형

멀티스레딩에서 발생하는 메모리 재정렬 현상과 해결 방법

예제 코드 소개

아래의 예제 코드는 두 개의 스레드가 각각 x와 y 변수를 설정하고, 다른 변수인 r1과 r2에 값을 할당하는 방식으로 동작합니다. 코드를 여러 번 실행해 보면, r1과 r2 값이 동시에 0이 되는 경우가 발생할 수 있습니다.

class Program
    {
        static int x = 0;
        static int y = 0;
        static int r1 = 0;
        static int r2 = 0;

        static void Thread_1()
        {
            y = 1;
            r1 = x;
        }

        static void Thread_2()
        {
            x = 1;
            r2 = y;
        }

        static void Main(string[] args)
        {
            int count = 0;

            while (true)
            {
                count++;

                x = y = r1 = r2 = 0;
                Task t1 = new Task(Thread_1);
                Task t2 = new Task(Thread_2);
                t1.Start();
                t2.Start();

                Task.WaitAll(t1, t2);

                if (r1 == 0 && r2 == 0)
                    break;
            }

            Console.WriteLine($"{count}번만에 빠져나옴!");
        }
    }

 

실행 횟수는 컴퓨터의 성능마다 크게 차이날 수 있습니다.

 

문제 설명: 메모리 재정렬 (Memory Reordering)

멀티스레드 환경에서 CPU는 성능 최적화를 위해 명령어 실행 순서를 변경할 수 있습니다. 이로 인해 코드에서 예상한 순서와 실제 실행 순서가 달라질 수 있습니다. 이를 메모리 재정렬이라고 합니다. 메모리 재정렬은 특히 멀티코어 프로세서에서 빈번하게 발생하며, 이는 메모리 일관성 모델(memory consistency model)에서 허용하는 행동입니다.

예제 코드의 문제

코드에서 두 스레드는 다음과 같은 순서로 명령을 실행할 수 있습니다:

  1. Thread_1이 y = 1을 실행
  2. Thread_2가 x = 1을 실행
  3. Thread_1이 r1 = x를 실행 (이 시점에서 x는 아직 0)
  4. Thread_2가 r2 = y를 실행 (이 시점에서 y는 아직 0)

이 경우 r1과 r2는 모두 0이 됩니다. 이는 두 스레드의 명령어 실행 순서가 서로 엇갈려서 발생합니다.

해결 방법: 메모리 배리어와 동기화

멀티스레드 환경에서 메모리 재정렬 문제를 해결하기 위해 메모리 배리어(memory barrier) 또는 동기화 기법을 사용할 수 있습니다. C#에서는 volatile 키워드와 lock 구문, 그리고 다양한 동기화 프리미티브를 제공하여 메모리 재정렬을 방지할 수 있습니다.

1. volatile 키워드 사용

volatile 키워드는 변수의 값이 여러 스레드에 의해 변경될 수 있음을 컴파일러와 런타임에 알려줍니다. 이는 해당 변수에 대한 모든 읽기 및 쓰기 작업이 메모리 배리어를 통해 순서대로 수행되도록 합니다.

static volatile int x = 0;
static volatile int y = 0;

2. lock 구문 사용

lock 구문을 사용하여 코드 블록에 대한 배타적 접근을 보장할 수 있습니다. 이는 특정 코드 블록 내에서 한 번에 하나의 스레드만 실행되도록 합니다.

static readonly object _lock = new object();

static void Thread_1()
{
    lock (_lock)
    {
        y = 1;
        r1 = x;
    }
}

static void Thread_2()
{
    lock (_lock)
    {
        x = 1;
        r2 = y;
    }
}

3. Interlocked 클래스 사용

Interlocked 클래스는 원자적(atomic) 연산을 제공합니다. 이는 변수의 값을 읽고 쓰는 작업을 하나의 불가분한 연산으로 처리합니다.

static void Thread_1()
{
    Interlocked.Exchange(ref y, 1);
    r1 = Interlocked.CompareExchange(ref x, 0, 0);
}

static void Thread_2()
{
    Interlocked.Exchange(ref x, 1);
    r2 = Interlocked.CompareExchange(ref y, 0, 0);
}

3. MemoryBarrier 사용

static void Thread_1()
        {
            y = 1;
            Thread.MemoryBarrier();
            r1 = x;
        }

        static void Thread_2()
        {
            x = 1;
            Thread.MemoryBarrier();
            r2 = y;
        }

CPU나 컴파일러에게 barrier 명령문 전 후의 메모리 연산을 순서에 맞게 실행하도록 강제하는 기능이다. 즉 barrier 이전에 나온 연산들이 barrier 이후에 나온 연산보다 먼저 실행이 되는게 보장되어야 하는 것이다.

결론

이번 포스트에서는 멀티스레딩 환경에서 발생할 수 있는 메모리 재정렬 문제와 이를 해결하기 위한 방법에 대해 알아보았습니다. 메모리 재정렬은 CPU의 성능 최적화를 위한 과정에서 발생하며, 멀티스레드 프로그램에서 예상치 못한 결과를 초래할 수 있습니다. 이를 방지하기 위해 volatile 키워드, lock 구문, Interlocked 클래스와 같은 동기화 기법을 사용하여 코드의 실행 순서를 보장해야 합니다.

멀티스레딩 프로그래밍은 복잡하지만, 올바른 동기화 기법을 사용하면 안정적이고 효율적인 코드를 작성할 수 있습니다. 추가적인 질문이나 궁금한 점이 있다면 언제든지 댓글로 남겨주세요!

반응형