쓰레드(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 }; // race condition
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
을 사용하는 방법도 있습니다.
참고자료
'프로그래밍 > C++' 카테고리의 다른 글
Visual studio Google Test 사용 예제 (0) | 2020.08.09 |
---|---|
C++ RAII (Resource Acquisition is initialization) (0) | 2020.07.26 |
C++ std::unique_ptr 2차원 배열 만들기 (0) | 2020.07.17 |
C++11 implicit narrowing conversion (축소 변환) 방지하는 법 (0) | 2020.06.23 |
C++ 배열의 값이 전부 같은지 확인하는 방법 (std::equal) (0) | 2020.06.20 |