Modern C++ - thread

진경천·2024년 3월 22일
0

C++

목록 보기
89/90

thread의 정의

#include <thread>

std::thread th([실행할 함수]);

프로세스 내에서 실제로 작업을 수행하는 주체로, CPU 코어에서 돌아가는 프로그램의 단위이다.

#include <iostream>
#include <thread>
#include <chrono>

using std::cout;
using std::endl;
using namespace std::chrono_literals;

int main() {
	cout << "thread ID : " << std::this_thread::get_id() << endl;

	std::thread th([] {
		cout << "thread ID : " << std::this_thread::get_id() << endl;
		std::this_thread::sleep_for(1s);
		cout << "thread complete" << endl;
		});

	th.join();
	// 조인이 없으면 에러
	std::thread th0 = std::move(th);
	// 복사가 안되서 이동시켜주어야 한다.

	cout << "Complete" << endl;
}

실행 결과

thread ID : 45796
thread ID : 43432
thread complete
Complete

thread를 생성한 후 th.join()을 이용해 thread를 실행 시켜주었다.
thread의 ID와 thread complete를 보면 thread가 실행되었다는 것을 알 수 있다.
또한, thread는 복사가 안되어 std::move()등을 이용해 이동대입을 시켜야 한다.

thread_local

각각의 thread에 독립적인 변수를 만들어 준다.
정적 변수와 같이 각 thread가 실행될 때 1번 초기화 되고 thread가 종료 될 때 1번 해제된다.

#include <iostream>
#include <thread>
#include <chrono>

using std::cout;
using std::endl;
using namespace std::chrono_literals;

struct Test {
	int num;
	Test(int num) : num(num) {
		cout << "Construct : " << num << endl;
	}

	~Test() {
		cout << "Destruct : " << num << endl;
	}
};

thread_local Test test(10);

void foo() {
	for (int i = 0; i < 10; i++) {
		static int num = 10;
		num++;
		cout << num << endl;
	}
}

int main() {
	std::thread(foo).join();
	foo();
}

실행 결과

Construct : 10
Construct : 10
11 12 13 14 15 16 17 18 19 20
Destruct : 10
21 22 23 24 25 26 27 28 29 30
Destruct : 10

위와 같이 static 변수는 처음 생성될 때 1번만 초기화 되기 때문에 thread가 실행 되어도 그 값을 유지하는 것을 알 수 있다.

void foo() {
	for (int i = 0; i < 10; i++) {
		thread_local int num = 10;
		num++;
		cout << num << ' ';
	}
	cout << endl;
}

실행 결과

Construct : 10
Construct : 10
11 12 13 14 15 16 17 18 19 20
Destruct : 10
11 12 13 14 15 16 17 18 19 20
Destruct : 10

하지만 for문의 num을 위와 같이 thread_local int로 선언해준다면
thread가 실행 될 때 1번 초기화 하고 종료될 때 해제되기 때문에
각 thread가 실행 될 때만 값을 유지하는 것을 알 수 있다.

multithread

한개의 프로세스는 최소 한개 이상의 thread로 구성되기 때문에 여러개의 thread로 구성될 수 있다.
이러한 프로그램을 multithread 프로그램이라고 한다.

같은 프로세스 내의 thread 끼리는 서로 같은 메모리를 공유하기 때문에 문제가 발생할 수 있다.

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>

using std::cout;
using std::endl;
using namespace std::chrono_literals;


int main() {
	std::mutex m;
	int num = 0;
	auto func = [&num, &m] {
		for (int i = 0; i < 1000000; i++) {
			num++;
		}
	};

	std::thread th0(func);
	std::thread th1(func);

	th0.join();
	th1.join();

	cout << num << endl;
}

실행 결과

1726118

2000000 이 나왔어야 하는데 이상한 값이 나왔다.

그 이유는 num이라는 전역 변술을 2개의 thread가 공유를 하며 num += 1 연산을 하였기 때문이다.

2개의 thread가 동시에 연산을 수행하게 되면
th는 num

mutex

#include <mutex>

std::mutex m;

여러 thread의 공유자원에 대한 동시 접근을 차단하는 역할을 한다.

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>

using std::cout;
using std::endl;
using namespace std::chrono_literals;

 
int main() {
	std::mutex m;
	int num = 0;
	auto func = [&num, &m] {
		for (int i = 0; i < 1000000; i++) {
			m.lock();
			num++;
			m.unlock();
		}
	};

	std::thread th0(func);
	std::thread th1(func);

	th0.join();
	th1.join();

	cout << num << endl;
}

실행 결과

2000000

각 thread가 실행 될때마다 m.lock()을 이용해 다른 thread의 접근을 차단하여 정상적인 결과가 나왔다.

m.unlock()은 다시 다른 thread의 접근을 허용하게 하는것으로 lock을한 후에는 반드시 unlock을 해주어야 한다.
그렇지 않으면 다른 thread가 실행되지 못하고 교착상태에 빠지게 된다.

예외 발생

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>

using std::cout;
using std::endl;
using namespace std::chrono_literals;

void foo(int& num) {
	num++;
	throw std::runtime_error("err");
}

int main() {
	std::mutex m;
	int num = 0;
	auto func = [&num, &m] {
		for (int i = 0; i < 1000000; i++) {
			try {
				m.lock();
				foo(num);
				m.unlock();
			}
			catch (...) {

			}
		}
		};

	std::thread th0(func);
	std::thread th1(func);

	th0.join();
	th1.join();

	cout << num << endl;
}

위 코드를 실행하게 되면 예외가 발생하여 m.unlock이 실행되지 못하고 교착상태에 빠지게 된다.

이와 같은 문제를 방지하기 위해std::lock_guard<std::mutex>을 사용할 수 있다.

위 코드의 for문을 아래와 같이 변경해준다면 2000000이 출력되는 것을 볼 수 있다.

for (int i = 0; i < 1000000; i++) {
	try {
		std::lock_guard<std::mutex> lock(m);
		foo(num);
	}
	catch (...) {

	}
}

교착 상태

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>

using std::cout;
using std::endl;
using namespace std::chrono_literals;

std::mutex m;

void foo() {
	std::lock_guard<std::mutex> lock(m);
 }

void goo() {
	std::lock_guard<std::mutex> lock(m);

	foo();
}

int main() {
	cout << "foo" << endl;

	goo();
}

위 코드를 실행하게 되면 goo()를 호출하고 foo()를 호출하는 과정에서 goo() lock이 풀리지 않게 되어 교착 상태에 빠지게 된다.

예제

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <atomic>
#include <queue>

using std::cout;
using std::endl;
using namespace std::chrono_literals;

std::mutex m;

void produce(std::mutex& m, std::condition_variable& cv, std::queue<int>& jobQueue) {
	while (true) {
		std::this_thread::sleep_for(100ms);
		{
			std::lock_guard<std::mutex> lock(m);
			jobQueue.push(1);
			cout << "produce : " << jobQueue.size() << endl;
		}
		cv.notify_one();	// waiting 하는 thread에 실행 명령
	}
}

void longTimeJob() {
	std::this_thread::sleep_for(200ms);
}

void consume(std::mutex& m, std::condition_variable& cv, std::queue<int>& jobQueue) {
	while (true) {
		{
			std::unique_lock<std::mutex> lock(m);
			if (jobQueue.empty()) {	// lost wakeup
				cv.wait(lock);
			}

			if (jobQueue.empty())	// superious wakeup
				continue;

			int result = jobQueue.front();
			jobQueue.pop();

			cout << "consume : " << jobQueue.size() << endl;
		}
	}
	longTimeJob();
}

int main() {
	std::mutex m;
	std::condition_variable cv;
	std::queue<int> jobQueue;
	std::thread producer(produce, std::ref(m), std::ref(cv), std::ref(jobQueue));
	std::thread consumer(consume, std::ref(m), std::ref(cv), std::ref(jobQueue));

	producer.join();
	consumer.join();

}

실행 결과

1
0
무한 반복

profile
어중이떠중이

0개의 댓글