본문 바로가기
프로그래밍/시스템 프로그래밍

Atomic

by 오늘의논리 2024. 3. 3.
728x90

 

#include <iostream>
#include <thread>

int32 sum = 0;

void Add()
{
    for (int i = 0; i < 100'0000; ++i)
    	++sum;
}

void Sub()
{
    for (int i = 0; i < 100'0000; ++i)
    	--sum;
}


int main()
{
    Add();
    Sub();

    return 0;
}

이상황에서 Sum 당연한 결과지만 0 것이다. 하지만 멀티쓰레드 환경에서 돌린다면 어떻게 될까?

 

#include <iostream>
#include <thread>

int32 sum = 0;

void Add()
{
    for (int i = 0; i < 100'0000; ++i)
    	++sum;
}

void Sub()
{
    for (int i = 0; i < 100'0000; ++i)
    	--sum;
}

int main()
{
    Add();
    Sub();

    std::thread t1(Add);
    std::thread t2(Sub);

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

    cout << sum << endl;

    return 0;

}

이렇게 돌렸을경우 환경에서는

값이 출력되었는데 매번 돌릴 때마다 다른 값이 출력된다. 이부분이 공유 데이터와 관련이 있다. 스택 메모리 같은 경우에는 각기 쓰레드마다 자신의 영역을 따로 가지고 있다. 하지만 힙이나 데이터 영역의 정보들은 공유를 하기 때문에 코드와 같이 전역변수 같은 데이터들은 쓰레드가 서로 경합하며 건드리게 된다. 자세하게 들어가면

++ 혹은 -- 해주는부분이 레지스터에서 3번에 걸쳐서 일어나는데 그부분은 패스하도록 하겠다.

 

멀티쓰레드에서는 데이터를 공용으로 같이 활용 있는 것은 장점이지만 문제는 데이터를 수정하거나 한군데에서는 수정하고 한군데에서는 읽거나  다양한 문제가 발생  있다. 그래서 ++, -- 3단계에 걸쳐 일어나지 않고  한번에 일어나게 할수 있도록 동기화를 해줘야 하는데 여러가지 동기화 기법이 있지만 아토믹한 버전으로 만들어 보도록 하겠다.

 

Atomic 동시성 프로그래밍에서 중요한 개념이다. 원자적 연산을 보장하는 데이터형을 선언할 있다. 원자적 연산이란, 여러 쓰레드가 동시에 접근하더라도 연산이 분할되지 않고 번에 완료되는 것을 의미한다.

 

Atomic으로 선언된 변수는 초기에만 값을 대입할 있다. 외에는 대입이 불가하며, 대신 값을 증가시키거나 감소시키는 역할만 수행한다. 이는 atomic으로 선언된 변수를 이용해 어떤 동적을 하는 중에는 다른 쓰레드들이 해당 atomic변수를 절대 사용하지 못하기 때문이다.

 

DB 관련 작업을 할때도 종종 atomic 이용하는 상황이 있는데 A라는 유저 인벤에서 무기를 빼고 B유저 인벤에서 무기를 추가한다. 이런 경우도 아토믹한 상황이 이루어 져야한다.

 

다른 방법도 많지만 c++ 표준에서 #include <atomic> 으로 추가 변수를 추가할때

Atomic<자료형> 변수명 으로 선언해 주면 된다.

#include <iostream>
#include <thread>
#include <atomic>

atomic<int32> sum = 0;

void Add()
{
    for (int i = 0; i < 100'0000; ++i)
        ++sum;
}



void Sub()
{
    for (int i = 0; i < 100'0000; ++i)
    	--sum;
}


int main()
{
    Add();
    Sub();

    std::thread t1(Add);
    std::thread t2(Sub);

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

    cout << sum << endl;

    return 0;

}

 

이렇게 아까의 sum 전역변수를 atomic 변수로 바꾼 실행 하면

이번엔 0 실행되는 것을 있다. 여기서 sum++, 혹은 sum-- atomic변수같지 않기에 sum.fetch_add(1); 혹은 sum.fetch_sub(1); 함수를 이용하는것이 좋다.

 

결론은 멀티쓰레드 환경에서 여러 쓰레드가 동시에 같은 메모리 위치에 접근하려고 하는 데이터 경쟁이 발생하는데 이러한 상황에서는 컴파일러가 메모리에서 값을 읽고, 레지스터로 불러와서 값을 변경 결과를 메모리에 다시 저장하는 과정 중에 다른 쓰레드가 해당 메모리 위치를 수정 하면서 문제가 발생한다.

 

따라서 이런상황에서 atomic 변수를 사용하여 원자적 연산을 보장해서 한번에 하나의 스레드만 해당연산을 수행하게 하여 데이터 경쟁을 방지한다. 이때 이 순서를 정하는 순서는 CPU에서 정해준다.

 

그렇다면 앞으로 서버프로그래밍을할때 Atomic변수로만 범벅을 해주면 된다는 생각을 하면 안된다. atomic 연산은 생각보다 느리게 작업되고 병목현상으로 문제가 발생할 수 있기 때문에 정말로 꼭 필요할때만 사용해야 한다.

728x90

'프로그래밍 > 시스템 프로그래밍' 카테고리의 다른 글

SpinLock  (1) 2024.03.05
DeadLock  (0) 2024.03.04
락 기초  (0) 2024.03.04
쓰레드 생성  (1) 2024.03.03
멀티 쓰레드  (0) 2024.03.03

댓글