[스레드의 병렬 처리]
#include <iostream>
int32 sum = 0;
void Add()
{
for (int32 i = 0; i < 1'000'000; i++)
{
sum++;
}
}
void Sub()
{
for (int32 i = 0; i < 1'000'000; i++)
{
sum--;
}
}
int main()
{
thread temp(Add); // 꿀팁팁 : Ctrl + D 하면 바로 아래줄에 코드 복붙
thread temp2(Sub);
temp.join();
temp2.join();
cout << sum << endl;
}
일반적인 경우로 Add(), Sub() 함수를 실행시키면 전역 변수 sum의 값은 0이 될 것입니다. 하지만 두 함수를 각 스레드의 엔트리 포인트로 할당한 뒤 실행해주면 sum이 0아 이니라 예상치못한 값이 나오는 것을 확인할 수 있습니다.
스레드는 프로세스에서 스택을 제외한 모든 영역을 공유하기 때문에 임계 영역에 대한 문제가 발생합니다. 어셈블리 모드로 Add() 함수를 확인해보겠습니다.
00007FF7924C2436 mov eax, dword ptr[sum(07FF7924CF470h)]
00007FF7924C243C inc eax
00007FF7924C243E mov dword ptr[sum(07FF7924CF470h)], eax
명령어는 순서대로 다음과 같습니다.
1. 어떤 주소(데이터 영역의 sum을 가리키는 중)에 있는 값을 eax 레지스터에 넣어라
2. eax 레지스터에 있는 값을 1 증가시켜라
3. 그 결과물을 다시 eax 레지스터에 넣어라
위의 순서를 sum++에 적용하면, 의사코드는 다음과 같아집니다.
void Add()
{
for (int32 i = 0; i < 1'000'000; i++)
{
// sum++;
int32 eax = sum;
eax = eax + 1;
sum = eax;
}
}
단순한 하나의 줄이 세 개로 나눠지는 이유는, CPU가 메모리를 가져와서 연산하고 반환하는 과정을 한 번에 수행할 수 없기 때문입니다(동시 연산 불가능).
Add와 Sub 함수가 병렬처리 되는 과정을 살펴보겠습니다.
1. 데이터 영역의 sum은 현재 0
2. Add() 호출 시 int eax = sum이 실행되면서 sum의 값 0이 eax에 들어감
3. 마침 Sub()도 동시에 int eax = sum을 실행하면서 eax가 0이 됨
4. Add()가 다음 로직을 먼저 수행한다고 하면, eax = eax + 1을 실행
5. 그리고 연산된 eax 값을 sum에 넣으면 sum은 1이 됨
6. 이때 Sub()도 연산을 마치고 sum에 값을 넣으면 1에 값을 덮어쓰면서 -1이 됨
7. 이 과정이 반복될 경우 숫자가 의도대로 계산되지 않고 꼬이게 됨
[atomic]
데이터를 공유하는 것은 큰 장점이지만, 값을 수정하면 위와 같은 문제가 발생합니다. 이를 위해 3단계로 나눠지는 연산을 하나로 묶어 처리할 수 있는 방법이 존재합니다. 바로 atomic을 활용하면 됩니다.
#include <iostream>
#include <thread>
#include <atomic>
atomic<int32> a_sum = 0;
int main()
{
}
atomic은 All-or-nothing입니다. DB에서 트랜잭션 개념을 설명할 때 원자성(atomically)을 얘기하곤 하는데, 동일한 의미를 가지고 동일한 동작을 수행하도록 해줍니다.
트랜잭션 원자성은 데이터베이스 트랜잭션이 모두 수행되거나 아예 수행되지 않는 '모두 아니면 전무(All or Nothing)' 특성을 의미
Windows에서 interlockAdd() 함수로 실행할 수 있지만, 이 또한 호환성 문제가 발생합니다. 이를 위해 C++에서 atomic을 include하면 사용할 수 있도록 기능을 만들어두었습니다. template 클래스이기 때문에 사용하고자 하는 겂을 감싸서 선언해주면 됩니다.
이렇게 감싸진 값은 세 단계로 나눠지는 일련의 과정을 하나의 단계로 묶어줍니다. 동일하게 전위/후위 연산자를 활용할 수 있지만, 이러면 atomic 변수인지 아닌지 알기 어렵기 때문에, 명시적 사용을 위하여 fetch_add() 함수를 활용해주면 좋습니다.
void Add()
{
for (int32 i = 0; i < 1'000'000; i++)
{
//sum++;
a_sum.fetch_add(1);
}
}
결과를 확인해보면 이전과 다르게 정상적으로 0이 출력되는 것을 확인할 수 있습니다. 데이터가 오고 가는 과정을 CPU 차원에서 관리해주기 때문에 Add() 함수의 세 단계에 로직이 다 끝나야 Sub() 함수가 다음으로 접근해서 작업을 수행할 수 있게 되는 것입니다.
atomic을 활용한 값을 어셈블리어로 확인해볼 경우 코드가 한 줄 추가된 것을 확인할 수 있습니다.
00007FF692BF24A6 mov r8d, 5
00007FF692BF24AC mov edx, 1
00007FF692BF24B1 lea rcx, [a_sum(07FF692BFF48Ch)]
00007FF692BF24B8 call std::_Atomic_integral<int, 4>::fetch_add(07FF692BF1532h)
'서버' 카테고리의 다른 글
| [Server] 스핀락(Spinlock)과 sleep (0) | 2025.11.14 |
|---|---|
| [Server] 데드락(DeadLock) (0) | 2025.03.17 |
| [Server] Lock(mutex) (0) | 2025.03.17 |
| [Server] 스레드 (0) | 2025.03.17 |