C++ std::thread


쓰레드(Thread)는 프로세스 내부 실행 단위입니다. 쓰레드는 각자 스택영역을 갖고 있고, 같은 프로세스 내 쓰레드는 Data, 힙 영역을 공유합니다.

멀티프로세스 프로그래밍을 하려면 프로세스간 통신(Inter-process communication, IPC)을 사용해야되는데 shared memory, message passing 방식을 사용해야 합니다. 좀 복잡하죠. 쓰레드는 공유 영역이 있어서 간편합니다. 물론 동시성 문제가 있는데 자세한 건 후술.

C++11에 std::thread가 추가됐습니다. 사용법을 알아봅시다

 

 

std::thread 생성 방법

쓰레드는 Callable object로 생성할 수 있습니다. (ex. (멤버)함수 포인터, function object, lambda 등)

#include <iostream>
#include <thread>

using namespace std;

void foo(int Z) {
	for (int i = 0; i < Z; ++i)
		cout << "Thread (function pointer)\n";
}

class thread_obj {
public:
	void operator()(int x) {
		for (int i = 0; i < x; ++i)
			cout << "Thread (functor)\n";
	}
};

int main() {
	auto f = [](int x) {
		for (int i = 0; i < x; ++i)
			cout << "Thread (lambda)\n";
	};

	thread th1(foo, 3);
	thread th2(thread_obj(), 3);
	thread th3(f, 3);

	th1.join();
	th2.join();
	th3.join();

	return 0;
}

 

 

join, detach

std::thread의 메소드중에 join, detach에 알아봅시다.

먼저 join은 *this(해당 쓰레드)가 실행을 끝낼 때까지 현재 쓰레드를 Blocking하는 함수입니다. 예를 들어 위 예제에서 join을 빼버리면 각각 5번씩 출력돼야 하는데 몇 번 출력하고 프로그램이 종료될겁니다.

detach는 해당 쓰레드의 실행을 thread 오브젝트에서 분리시키는 함수입니다. 한번 분리시키면 쓰레드가 독립적으로 실행됩니다.

 

#include <iostream>
#include <thread>

using namespace std;

int main() {
	int cnt{ 0 };
	auto f = [&] { for (int i=0; i<1000000; ++i) cnt++; };
	thread t(f);
	
	t.detach();

	this_thread::sleep_for(std::chrono::milliseconds(1));
	cout << cnt << '\n';	// ?

	return 0;
}

이 프로그램의 출력은 어떻게 될까요? CPU 연산 속도가 충분히 빠르다면 100만이 나올 겁니다. 그렇지 않다면 0~100만 사이의 수가 어느정도 랜덤하게 나올 겁니다.

 

 

예제) 배열의 합 구하기

#include <iostream>
#include <thread>
#include <numeric>
#include <vector>

using namespace std;


typedef long long ll;

void adder(vector<int>::iterator start, vector<int>::iterator end, ll& result) {
	for (auto it = start; it != end; ++it)
		result += *it;

	auto thread_id = this_thread::get_id();
	cout << "thread id: " << thread_id << ", partial sum: " << result << '\n';
}

int main() {
	vector<int> arr(100000);
	for (int i=0; i<100000; ++i)
		arr[i] = (i+1);

	vector<ll> partial_sum(4);

	vector<thread> workers;
	for (int i = 0; i < 4; ++i)
		workers.emplace_back(adder, arr.begin()+i*25000,
			arr.begin()+(i+1)*25000, std::ref(partial_sum[i]));

	// Wait until every thread is over.
	for (auto& w : workers)
		w.join();

	cout << "====\n";
	cout << accumulate(partial_sum.begin(), partial_sum.end(), 0LL) << '\n';

	return 0;
}
// possible output
thread id: thread id: 9972, partial sum: thread id: 1812, partial sum: 2187512500
7820, partial sum: 312512500
thread id: 22264, partial sum: 937512500
1562512500
====
5000050000

1~10만까지의 합을 구하는 예제입니다. 1에서 10만까지의 합은 \(\displaystyle \sum_{k=1}^{10^5}k = \frac{10^5(10^5 + 1)}{2} = 5000050000 \)입니다.

10만칸 배열을 만든 뒤 쓰레드를 4개 만들고 4등분해서 각각의 쓰레드가 합치게 합니다. 그 다음 4개 쓰레드의 join을 호출해 쓰레드 별 실행이 끝날 때까지 기다리고, 마지막으로 쓰레드가 계산한 값들을 더해 답을 구합니다.

 

출력 예시를 보면 출력이 중구난방으로 돼있습니다. 순차적으로 실행했을 때와는 다르죠. 각각 독립적으로 실행되기 때문입니다.

쓰레드에 함수 파라미터로 레퍼런스를 넘길 때 그냥 넘기면 안되고, std::ref로 감싸줘야 합니다.

 

 

동시성(Concurrency) 문제 / Race Condition (경쟁상태)

#include <iostream>
#include <thread>

using namespace std;

int main() {
	int cnt = 0;
	auto f = [&] { for (int i=0; i<10000; ++i) cnt++; };
	std::thread t1{ f }, t2{ f }, t3{ f }; // undefined behavior

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

	cout << cnt << '\n';

	return 0;
}

여러 쓰레드가 동시에 한 값을 수정하는 경우 의도치 않은 결과가 나올 수 있습니다.

쓰레드 3개를 만들고 각 쓰레드가 한 변수에 ++을 1만번씩 하는 경우를 생각해봅시다

 

예상 결과는 30000인데 그 값보다 적은 값이 나왔네요

변수(=공유 영역, Critical Section)에 여러 쓰레드가 동시에 접근할 때 이런 일이 발생합니다.

 

Critical Section에 여러 쓰레드가 동시에 접근하지 못하게 하는 방법으로는 mutex, semaphore등으로 lock, free를 하는 방법이 있겠습니다.

멀티스레드 프로그래밍에서 동시성 제어는 정말 중요합니다. 나중에 게시물로 작성하겠습니다

 

#include <atomic>

atomic<int> cnt{ 0 };
auto f = [&] { for (int i=0; i<10000; ++i) cnt++; };
std::thread t1{ f }, t2{ f }, t3{ f }; // ok

std::atomic을 사용하는 방법도 있습니다.

 

 

참고자료

반응형