C++ 게임 서버 프로그래밍: 멀티스레드 환경에서 CPU 파이프라인 이해하기(메모리 재배치)

나무에물주기·2023년 6월 21일
1
post-thumbnail

필요한 헤더 파일들은 다음과 같습니다.

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>
#include <atomic>
#include <mutex>
#include <windows.h>
#include <future>

멀티스레드 환경에서 CPU 파이프라인 최적화로 인한 메모리 순서 재배치 현상에 대해 알아보겠습니다.

int32 x = 0;
int32 y = 0;
int32 r1 = 0;
int32 r2 = 0;

volatile bool ready;

void Thread_1()
{
	while (!ready)
		;

	y = 1;
	r1 = x;
}

void Thread_2()
{
	while (!ready)
		;

	x = 1;
	r2 = y;
}

int main()
{
	int32 count = 0;

	while (true)
	{
		ready = false;

		count++;

		x = y = r1 = r2 = 0;

		thread t1(Thread_1);
		thread t2(Thread_2);

		ready = true;

		t1.join();
		t2.join();

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

	cout << count << " 번만에 빠져나옴." << '\n';
}

위 코드는 Thread_1과 Thread_2라는 두 개의 스레드를 생성하고, 각 스레드에서는 공유 변수 x와 y를 변경한 후 다른 스레드에서 변경한 변수의 값을 읽습니다. 이 코드는 CPU 파이프라인의 최적화로 인한 메모리 순서 재배치가 발생할 경우, 두 스레드가 동시에 실행되면 r1과 r2 모두 0의 값을 가질 수 있음을 보여줍니다.

메모리 재배치 처리 방법

원자적(Atomic) 연산 사용

원자적 연산은 그 연산이 실행되는 동안에는 다른 어떤 연산도 동시에 수행되지 않으므로, 해당 연산이 완료된 후에만 다른 연산이 수행됩니다. 이러한 특성 때문에 원자적 연산은 멀티스레드 환경에서 변수에 안전하게 접근하는 방법으로 자주 사용됩니다. C++에서는 std::atomic 라이브러리를 통해 원자적 연산을 지원합니다.

#include <atomic>
#include <thread>

std::atomic<int> x(0), y(0);
int r1, r2;

void Thread_1() {
    x.store(1, std::memory_order_relaxed); // 저장
    r1 = y.load(std::memory_order_relaxed); // 불러오기
}

void Thread_2() {
    y.store(1, std::memory_order_relaxed); // 저장
    r2 = x.load(std::memory_order_relaxed); // 불러오기
}

int main() {
    std::thread t1(Thread_1);
    std::thread t2(Thread_2);

    t1.join();
    t2.join();
}

이 코드는 std::atomic을 사용해서 x와 y의 순서를 보장하고 있습니다. std::memory_order_relaxed를 사용해서 메모리 순서를 자유롭게 하였습니다.

뮤텍스(Mutex) 사용

뮤텍스는 상호 배제(mutual exclusion)를 보장하는 기능입니다. 한 스레드가 뮤텍스를 소유하고 있을 때는 다른 스레드가 그 뮤텍스를 소유할 수 없습니다. 따라서 뮤텍스를 이용하면 여러 스레드가 동시에 동일한 리소스에 접근하는 것을 막을 수 있습니다. C++에서는 std::mutex 클래스를 통해 뮤텍스를 사용할 수 있습니다.

#include <mutex>
#include <thread>

std::mutex mtx;
int x = 0, y = 0;
int r1, r2;

void Thread_1() {
    std::lock_guard<std::mutex> lock(mtx);
    x = 1;
    r1 = y;
}

void Thread_2() {
    std::lock_guard<std::mutex> lock(mtx);
    y = 1;
    r2 = x;
}

int main() {
    std::thread t1(Thread_1);
    std::thread t2(Thread_2);

    t1.join();
    t2.join();
}

이 코드는 std::mutex를 사용해서 Thread_1과 Thread_2가 동시에 실행되지 않도록 보장하고 있습니다. std::lock_guard는 생성될 때 std::mutex를 잠그고, 소멸될 때 std::mutex를 풀어줍니다. 따라서, 각 스레드의 Thread_1과 Thread_2가 동시에 실행되지 않음을 보장합니다.

profile
개인 공부를 정리함니다

0개의 댓글