반응형

메모리 배리어 ( Memory Barrier )

중앙 처리 장치나 컴파일에게 특정 연산의 순서를 강제하도록 하는 기능이다.

컴파일러를 중점적으로 보자면, 컴파일러에서 최적화를 수행한다.

하지만, 이러한 기능은 여러 스레드가 돌아가는 경우에는 코드의 실행 순서가 바뀌어 실행되는 동안 다른 스레드에서 그 부분에 대한 메모리를 접근하여 잘못된 결과를 내놓을 수 있다.

따라서 특정 부분에 실행 순서를 강제하는 메모리 배리어를 놓아야한다.

 

예제 ( C# )

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

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

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

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

        while (true)
        {
            x = y = r1 = r2 = 0;
            count++;

            Task t1 = new Task(Thread_1);
            Task t2 = new Task(Thread_2);

            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2); // 제공된 모든 Task 개체의 실행이 완료되기를 기다립니다.

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

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

메모리 연산인 store과 load를 하는 Thread1, Thread2가 존재하고 Main함수 안에서 실행시킨다.

위의 동작을 보면 순서대로 보면 초기에 0으로 설정되어 있는 x, y가 1로 바뀌고 r1, r2에 1이 할당되면서 while문이 계속 반복되어 무한 루프를 도는 것이다.

하지만 r1 == 0 && r2 == 0 이 되어 break 되는 경우가 생긴다.

아마 이것을 모르고 겪었다면 머리가 깨졌을지도 모른다.

x = 0 // 반복문 안
y = 0

x = 1 // 쓰레드 안
y = 1 // 쓰레드 안

r1 = x // 쓰레드 안
r2 = y

실제로 코드를 짤 때, 이렇게 순서대로 실행될 것이라고 예상하지만, 이런 경우에는 절대 r == 0 && r2 == 0이 되는 경우가 나올 수 없다. 그렇다면 어떻게 된것일까?

x = 0 // 반복문 안
y = 0

r1 = x // 쓰레드 안
r2 = y 

// 여기서 반복문 빠져 나오게 됨.

x = 1 // 쓰레드 안
y = 1

실제로는 위와 같이 수행되고 있는 것이다.

 

컴파일러에서 성능의 최적화를 위해서 코드의 순서를 바뀌어 실행시킬 수 있는데, 싱글 쓰레드에서는 문제가 되지 않지만, 멀티 쓰레드에서는 문제가 될 수 있다. 

따라서 이를 방지 하기 위해서는 명령어를 순서대로 실행시키기 위한 메모리 배리어를 사용한다.

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

        static void Thread_1()
        {
            y = 1; // Store 
            Thread.MemoryBarrier(); // 메모리 배리어
            r1 = x; // Load
        }

        static void Thread_2()
        {
            x = 1; // Store
            Thread.MemoryBarrier(); // 메모리 배리어
            r2 = y; // Load 
        }

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

            while (true)
            {
                x = y = r1 = r2 = 0;
                count++;

                Task t1 = new Task(Thread_1);
                Task t2 = new Task(Thread_2);

                t1.Start();
                t2.Start();

                Task.WaitAll(t1, t2); // 제공된 모든 Task 개체의 실행이 완료되기를 기다립니다.

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

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

위와 같이 Store와 Load 사이에 메모리 배리어를 설정하면 배리어 위의 코드(Store)와 아래의 코드(Load) 순서가 바뀌어 실행될 수 없게 된다. 이렇게 코드의 실행 순서를 보장해주는 것이 메모리 배리어의 역할이다.

메모리 배리어의 용도

1. 코드의 재배치를 억제하는 기능 : 위의 예시를 통해 봤듯이 컴파일러 최적화로 코드 재배치 되는 것을 막아준다.

2. 가시성 : Thread에서 변경한 특정 메모리 값이 다른 Thread에서 제대로 읽어지는 것이라고 할 수 있다.

 

메모리 배리어의 종류

1. Full Memory Barrier : read/write  둘다 막는다.

2. Store Memory Barrier : writer만 막는다.

3. Load Memory Barrier : read만 막는다.

volatile, atomic, lock 같은 개념들도 내부적으로 Memory Barrier가 구현이 되어 있다.메모리 배리어는 하드웨어 개념이다.

반응형

'Etc > Computer Science' 카테고리의 다른 글

[CS] 캐시 이론  (0) 2022.07.10

+ Recent posts