Server/C++

쓰레드 (Thread)와 Atomic

Juzdalua 2024. 7. 29. 01:34

CPU 코어가 직접 할당되어 구동되는 흐름의 단위.

모든 스레드는 Heap, Data영역에 접근할 수 있다.

멀티 스레드는 프로그램을 병렬로 실행시키는 것과 같다.

 

CPU 코어 갯수보다 많은 스레드를 생성하고 할당하면 이론대로 작동하지 않는다.

// 주로 사용하는 함수

t.join();
t.joinable();

 

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"

#include <thread>

void HelloThread() {
	cout << "Hello Thread" << endl;
}

void HelloThread2(int32 num) {
	cout << num << endl;
}

void HelloThread3(int32 num, int32 num2) {
	cout << num << " " << num2 << endl;
}

int main() // 메인 스레드
{
	//thread t(HelloThread);
	thread t;
	// t.get_id(); == 0
	t = thread(HelloThread); // 실제 스레드 생성. id 배정

	cout << "Hello Main" << endl;

	int32 count = t.hardware_concurrency(); // CPU 코어 개수 추측 함수.
	auto id = t.get_id(); // 스레드마다 고유하게 부여되는 id
	if (t.joinable()) // 실제 스레드가 연결이 되어있는지 확인.
		t.join(); // t 스레드가 종료될 때까지 메인 스레드를 종료하지 않고 기다린다.
	t.detach(); // 스레드 객체 t에서 실제 스레드를 분리한다. -> 이후 t의 상태나 정보를 활용할 수 없게된다. 거의 사용하지 않음.

	// 스레드는 병렬로 실행되기때문에 순서를 보장하지 않는다.
	vector<thread> v;
	for (int32 i = 0; i < 5; i++) {
		v.push_back(thread(HelloThread2, i));
	}

	for (int32 i = 0; i < 5; i++) {
		if (v[i].joinable()) v[i].join();
	}

	thread t3(HelloThread3, 3, 4);
	t3.join();
}

멀티스레드 병렬처리의 문제

멀티스레드는 Heap 또는 Data 영역에 위치한 하나의 데이터를 공유할 수 있는 장점이 있다.

하지만 멀티스레드간 순서가 보장되지 않으므로 원하는 결과값이 나올 수 없는 단점이 있다.

 

아래는 전역변수 sum을 데이터영역에 선언하고 두개의 다른 스레드에서 공동 작업을 하는 코드이다.

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>

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() // 메인 스레드
{
	Add();
	Sub();
	cout << sum << endl;

	thread t1(Add);
	thread t2(Sub);
	t1.join();
	t2.join();
	cout << sum << endl;
}

 

하나의 메인스레드에서 실행하면 sum의 결과는 항상 0이지만, 멀티스레드로 동시작업을 진행하면 값이 항상 다르다.

 

sum을 증가시키는 위치에 break point를 잡고 디스어셈블리언어로 확인해보았다.

디버그 - 창 - 디스어셈블리

sum++; 작업은 3단계로 이루어진다.

1. 메모리에서 sum의 주소를 eax 레지스터로 이동한다.

2. eax의 값을 증가시킨다.

3. eax에 저장된 sum을 원래 주소로 이동시킨다.

위 단계를 코드로 표현하면 다음과 같다.

eax = sum;
sum++;
sum = eax;

 

멀티스레드 환경에서 i가 0일 경우 Add, Sub 함수가 한 번씩 실행된다고 가정했을 경우 위 코드와 같이 순차적으로 작동한다는 보장은 없다.

즉 i가 0을 순회하고 1이 되었을 때, sum의 값은 1 또는 -1 또는 0일 것이다.

sum = 0

Add() -> eax = sum
Add() -> eax = 1
Add() -> sum = eax
sum = 1
Sub() -> eax = sum
Sub() -> eax = 0
Sub() -> sum = eax -> sum = 0
sum = 0

-----------------------------------
sum = 0

Add() -> eax = sum
Add() -> eax = 1
Sub() -> eax = sum
Sub() -> eax = -1
Add() -> sum = eax -> sum = 1
Sub() -> sum = eax -> sum = -1
sum = -1

-----------------------------------
sum = 0

Sub() -> eax = sum
Sub() -> eax = -1
Add() -> eax = sum
Add() -> eax = 1
Sub() -> sum = eax -> sum = -1
Add() -> sum = eax -> sum = 1

sum = 1

 

이론상 위 문제를 해결하려면 코드간 순서가 보장되거나 하나의 코드에서 실행되어야 한다.

실질적으로 이를 해결하기 위한 방법으로 동기화 시키는 작업이 있다.

atomic 연산은 모두 실행되거나 아예 실행이 되지 않는 상황만 존재할 수 있는 표현이다.

 

Atomic 동기화 방법

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

atomic<int32> sum = 0;

void Add() {
	for (int32 i = 0; i < 1'000'000; i++) {
		sum.fetch_add(1);
	}
}

void Sub() {
	for (int32 i = 0; i < 1'000'000; i++) {
		sum.fetch_sub(1);
	}
}

int main() // 메인 스레드
{
	Add();
	Sub();
	cout << sum << endl;

	thread t1(Add);
	thread t2(Sub);
	t1.join();
	t2.join();
	cout << sum << endl;
}

 

atomic 템플릿은 공유 데이터를 하나의 스레드에서 작업이 발생할 시 해당 작업시 완료될 때까지 기다린 후 다음 스레드에서 공유 데이터 작업을 실행하도록 CPU가 작동한다.

 

atomic 연산은 멀티스레드 환경에서 용이하게 사용할 수 있지만, 연산속도가 느리므로 모든 상황에서 사용하지는 않아야한다.

'Server > C++' 카테고리의 다른 글

데드락 ( Dead Lock )  (0) 2024.07.29
Lock  (0) 2024.07.29
STL) Map과 Set  (0) 2024.07.28
STL) Vector와 List - iterator  (0) 2024.07.27
STL) Vector와 List  (0) 2024.07.27