이번 강좌에서는
에 대해 다룹니다. 안녕하세요 여러분! 지난 강좌에서 보았듯이, 서로 다른 쓰레드에서 같은 메모리를 공유할 때 발생할 수 있는 문제를 보았습니다. 이와 같이 서로 다른 쓰레드들이 동일한 자원을 사용할 때 발생하는 문제를 경쟁 상태(race condtion) 이라 부릅니다. 이 경우 그 코드를 다시 가져오면 아래와 같습니다. #include <iostream> #include <thread> #include <vector> void worker(int& counter) { for (int i = 0; i < 10000; i++) { counter += 1; } } int main() { int counter = 0; std::vector<std::thread> workers; for (int i = 0; i < 4; i++) { // 레퍼런스로 전달하려면 ref 함수로 감싸야 한다 (지난 강좌 bind 함수 참조) workers.push_back(std::thread(worker, std::ref(counter))); } for (int i = 0; i < 4; i++) { workers[i].join(); } std::cout << "Counter 최종 값 : " << counter << std::endl; } 성공적으로 컴파일 하였다면 실행 결과 Counter 최종 값 : 26459 왜 이런 문제가 발생하였을까요? counter += 1; 문제는 위 명령에 있습니다. 컴퓨터에 입장에서 생각해봅시다. 이를 이해하기 위해서는 CPU 에서 연산을 어떻게 처리하는지 알아야 합니다. CPU 는 말했듯이 컴퓨터의 모든 연산이 발생하는 두뇌 라고 볼 수 있습니다. CPU 에서 연산을 수행하기 위해서는, CPU 의 레지스터(register) 라는 곳에 데이터를 기록한 다음에 연산을 수행해야 합니다. 레지스터의 크기는 매우 작습니다. 64 비트 컴퓨터의 경우, 레지스터의 크기들이 8 바이트에 비해 불과합니다. 뿐만 아니라 레지스터의 개수는 그리 많지 않습니다. 일반적인 연산에서 사용되는 범용 레지스터의 경우 불과 16 개 밖에 없습니다. 메인보드를 보면 CPU 바로 옆에 메모리가 있습니다즉, 모든 데이터들은 메모리에 저장되어 있고, 연산 할 때 할 때 마다 메모리에서 레지스터로 값을 가져온 뒤에, 빠르게 연산을 하고, 다시 메모리에 가져다 놓는 식으로 작동을 한다고 보시면 됩니다. 쉽게 말하자면, 메모리는 냉장고 이고 CPU 의 레지스터는 도마 라고 보시면 됩니다. 냉장고 (RAM) 에서 재료를 도마 위에 하나 (레지스터) 꺼내서 후다닥 썰고 (연산) 다시 냉장고로 가져다 놓는 거라 생각하면 됩니다. 그렇다면 mov rax, qword ptr [rbp - 8] mov ecx, dword ptr [rax] add ecx, 1 mov dword ptr [rax], ecx 흠, 조금 무섭게 생겼습니다. 위와 같은 코드를 어셈블리(Assembly) 코드라고 부릅니다. 어셈블리 코드는 CPU 가 실제로 실행하는 기계어와 1 대 1 대응이 되어 있습니다. 따라서, 위 명령을 한줄 한줄 CPU 가 처리한다고 생각해도 무방합니다. 이해하기 매우 어렵게 생겼지만 사실 하나씩 뜯어보면 크게 어렵지 않습니다. 먼저 첫번째 줄 부터 살펴봅시다. mov rax, qword ptr [rbp - 8]
이 때 즉, C++ 의 언어로 풀어 쓰자면 rax = *(int**)(rbp - 8) 가 되겠습니다. 실제로 위 명령에서 무슨 짓을 하고 있는 것이냐면 현재 mov ecx, dword ptr [rax] 현재 ecx = *(int*)(rax); // rax 에는 &result 가 들어가 있음 와 동일합니다. 자 이제 그 다음 문장 입니다. add ecx, 1 언뜩 봐도 알 수 있듯이 mov dword ptr [rax], ecx 마지막으로 참고로 mov rax, qword ptr [rbp - 8] add dword ptr [rax], 1 이렇게 하면 안되냐고 생각할 수 있는데, 이는 CPU 의 구조상 add 명령은 역참조한 메모리에서 직접 사용할 수 없고 반드시 레지스터에만 내릴 수 있습니다. (냉장고 안에서 직접 요리를 할 수 없으니까요!) 자 그러면, 왜 이제 위 그림과 같은 상황을 생각해봅시다. 처음에 mov rax, qword ptr [rbp - 8] mov ecx, dword ptr [rax] 딱 여기 까지 실행하였다고 생각해봅시다. 그러면 이 시점에서 쓰레드 1 의 다음에 쓰레드 2 에서 전체 명령을 모두 실행합니다. 현재 쓰레드 1 이 다시 쓰레드 1 의 차례 입니다. 쓰레드 1 에서 나머지 add ecx, 1 mov dword ptr [rax], ecx 부분을 실행하였습니다. 이 때 쓰레드 1 의 물론 운이 좋다면 쓰레드 1 에서 중간에 쓰레드 2 가 실행되는 일 없이 쭉 실행해서 정상적으로 이게 멀티쓰레딩의 재밌는 점입니다. 여태까지 여러분이 실행한 모든 프로그램은 몇 번을 실행 하건 결과가 동일하게 나왔습니다. 하지만, 멀티 쓰레드 프로그램의 경우 프로그램 실행 마다 그 결과가 달라질 수 있습니다. 이게 무슨 말일까요? 제대로 프로그램을 만들지 않았을 경우 디버깅이 겁나 어렵다는 뜻입니다. 그렇다면 위 문제를 어떻게 하면 해결할 수 있을까요? 위 문제가 발생한 근본적인 이유는 counter += 1; 위 부분을 여러 쓰레드에서 동시에 실행시켰기 때문이지요. 그렇다면 만약에 어떤 경찰관 같은 역할을 하는 것이 있어서, 한 번에 한 쓰레드에서만 위 코드를 실행시킬 수 있다면 어떨까요? 쓰레드 한 개만 들어와!그렇다면 우리가 앞서 말한 문제를 완벽히 해결할 수 있을 것입니다. 그리고 다행이도 C++ 에선 이러한 기능을 하는 객체를 제공하고 있습니다. 바로 뮤텍스(mutex) 라고 불리는 것입니다. #include <iostream> #include <mutex> // mutex 를 사용하기 위해 필요 #include <thread> #include <vector> void worker(int& result, std::mutex& m) { for (int i = 0; i < 10000; i++) { m.lock(); result += 1; m.unlock(); } } int main() { int counter = 0; std::mutex m; // 우리의 mutex 객체 std::vector<std::thread> workers; for (int i = 0; i < 4; i++) { workers.push_back(std::thread(worker, std::ref(counter), std::ref(m))); } for (int i = 0; i < 4; i++) { workers[i].join(); } std::cout << "Counter 최종 값 : " << counter << std::endl; } 성공적으로 컴파일 하였다면 실행 결과 Counter 최종 값 : 40000 와 같이 제대로 나오는 것을 알 수 있습니다. std::mutex m; // 우리의 mutex 객체 일단 위와 같이 뮤텍스 객체를 정의 하였습니다. void worker(int& result, std::mutex& m) 뮤텍스를 각 쓰레드에서 사용하기 위해 위와 같이 전달하였고; m.lock(); result += 1; m.unlock(); 실제 사용하는 것은 위와 같습니다.
따라서, 이렇게 만약에 까먹고 #include <iostream> #include <mutex> // mutex 를 사용하기 위해 필요 #include <thread> #include <vector> void worker(int& result, std::mutex& m) { for (int i = 0; i < 10000; i++) { m.lock(); result += 1; } } int main() { int counter = 0; std::mutex m; // 우리의 mutex 객체 std::vector<std::thread> workers; for (int i = 0; i < 4; i++) { workers.push_back(std::thread(worker, std::ref(counter), std::ref(m))); } for (int i = 0; i < 4; i++) { workers[i].join(); } std::cout << "Counter 최종 값 : " << counter << std::endl; } 성공적으로 컴파일 하였다면 실행 결과 (끝나지 않아서 강제 종료) 와 같이 나옵니다. 위와 같이 프로그램이 끝나지 않아서 강제로 종료해야만 합니다. 뮤텍스를 취득한 쓰레드가 결국 아무 쓰레드도 연산을 진행하지 못하게 됩니다. 이러한 상황을 데드락(deadlock) 이라고 합니다. 위와 같은 문제를 해결하기 위해서는 취득한 뮤텍스는 사용이 끝나면 반드시 반환을 해야 합니다. 하지만 코드 길이가 길어지게 된다면 반환하는 것을 까먹을 수 있기 마련입니다. 곰곰히 생각해보면 이전에 비슷한 문제를 해결한 기억이 있습니다. 뮤텍스도 마찬가지로 사용 후 해제 패턴을 따르기 때문에 동일하게 소멸자에서 처리할 수 있습니다. #include <iostream> #include <mutex> // mutex 를 사용하기 위해 필요 #include <thread> #include <vector> void worker(int& result, std::mutex& m) { for (int i = 0; i < 10000; i++) { // lock 생성 시에 m.lock() 을 실행한다고 보면 된다. std::lock_guard<std::mutex> lock(m); result += 1; // scope 를 빠져 나가면 lock 이 소멸되면서 // m 을 알아서 unlock 한다. } } int main() { int counter = 0; std::mutex m; // 우리의 mutex 객체 std::vector<std::thread> workers; for (int i = 0; i < 4; i++) { workers.push_back(std::thread(worker, std::ref(counter), std::ref(m))); } for (int i = 0; i < 4; i++) { workers[i].join(); } std::cout << "Counter 최종 값 : " << counter << std::endl; } 성공적으로 컴파일 하였다면 실행 결과 Counter 최종 값 : 40000 와 같이 나옵니다. std::lock_guard<std::mutex> lock(m);
따라서 사용자가 따로 그렇다면 아래와 같은 상황을 생각해봅시다. #include <iostream> #include <mutex> // mutex 를 사용하기 위해 필요 #include <thread> void worker1(std::mutex& m1, std::mutex& m2) { for (int i = 0; i < 10000; i++) { std::lock_guard<std::mutex> lock1(m1); std::lock_guard<std::mutex> lock2(m2); // Do something } } void worker2(std::mutex& m1, std::mutex& m2) { for (int i = 0; i < 10000; i++) { std::lock_guard<std::mutex> lock2(m2); std::lock_guard<std::mutex> lock1(m1); // Do something } } int main() { int counter = 0; std::mutex m1, m2; // 우리의 mutex 객체 std::thread t1(worker1, std::ref(m1), std::ref(m2)); std::thread t2(worker2, std::ref(m1), std::ref(m2)); t1.join(); t2.join(); std::cout << "끝!" << std::endl; } 성공적으로 컴파일 하였다면 실행 결과 (끝나지 않아서 강제 종료) 와 같이 나옵니다. 위와 같이 프로그램이 끝나지 않아서 강제로 종료해야만 합니다. 왜 이런 일이 발생하였을까요?
std::lock_guard<std::mutex> lock1(m1); std::lock_guard<std::mutex> lock2(m2); 와 같이 std::lock_guard<std::mutex> lock2(m2); std::lock_guard<std::mutex> lock1(m1);
그렇다면 다음과 같은 상황을 생각해보세요. 만약에 아닙니다. 즉 여기에 보면 데드락이 발생하는 조건이 잘 나타나 있습니다. 물론 만족해야 할 조건이 꽤나 많지만, 일어날 수 있는 일은 반드시 일어나고, 데드락 때문에 디버깅 하는 것 만큼 골때리는 것도 없습니다. 그렇다면 데드락이 가능한 상황을 어떻게 해결할 수 있을까요? 한 가지 방법으로는 한 쓰레드에게 우선권을 주는 것입니다. 위 자동차 그림으로 보자면 초록색 차가 노란색 차보다 항상 먼저 지나가도록 우선권을 주는 것이지요. 만약에 노란색 차가 교차로에 있는데 초록색 차가 들어온다면 초록색 차가 노란색 차에게 "야 차 빼~!" 라고 요구할 수 도 있지요. 물론 노란색 차는 억울하겠지만, 적어도 차들이 뒤엉켜서 아무도 전진하지 못하는 상황은 막을 수 있습니다. 쓰레드로 비유하자면, 한 쓰레드가 다른 쓰레드에 비해 우위를 갖게 된다면, 한 쓰레드만 열심히 일하고 다른 쓰레드는 일할 수 없는 기아 상태(starvation)가 발생할 수 있습니다. 위에서 말한 해결 방식을 코드로 옮기자면 아래와 같습니다. #include <iostream> #include <mutex> // mutex 를 사용하기 위해 필요 #include <thread> void worker1(std::mutex& m1, std::mutex& m2) { for (int i = 0; i < 10; i++) { m1.lock(); m2.lock(); std::cout << "Worker1 Hi! " << i << std::endl; m2.unlock(); m1.unlock(); } } void worker2(std::mutex& m1, std::mutex& m2) { for (int i = 0; i < 10; i++) { while (true) { m2.lock(); // m1 이 이미 lock 되어 있다면 "야 차 빼" 를 수행하게 된다. if (!m1.try_lock()) { m2.unlock(); continue; } std::cout << "Worker2 Hi! " << i << std::endl; m1.unlock(); m2.unlock(); break; } } } int main() { std::mutex m1, m2; // 우리의 mutex 객체 std::thread t1(worker1, std::ref(m1), std::ref(m2)); std::thread t2(worker2, std::ref(m1), std::ref(m2)); t1.join(); t2.join(); std::cout << "끝!" << std::endl; } 성공적으로 컴파일 하였다면 실행 결과 Worker1 Hi! 0 Worker1 Hi! 1 Worker1 Hi! 2 Worker1 Hi! 3 Worker1 Hi! 4 Worker1 Hi! 5 Worker1 Hi! 6 Worker1 Hi! 7 Worker1 Hi! 8 Worker1 Hi! 9 Worker2 Hi! 0 Worker2 Hi! 1 Worker2 Hi! 2 Worker2 Hi! 3 Worker2 Hi! 4 Worker2 Hi! 5 Worker2 Hi! 6 Worker2 Hi! 7 Worker2 Hi! 8 Worker2 Hi! 9 끝! 데드락 상황 없이 잘 실행됨을 알 수 있습니다. (물론 출력하는 개수가 적어서 그럴 수 도 있습니다. m1.lock(); m2.lock(); std::cout << "Worker1 Hi! " << i << std::endl; m2.unlock(); m1.unlock(); 일단 while (true) { m2.lock(); // m1 이 이미 lock 되어 있다면 "야 차 빼" 를 수행하게 된다. if (!m1.try_lock()) { m2.unlock(); continue; } std::cout << "Worker2 Hi! " << i << std::endl; m1.unlock(); m2.unlock(); break; }
만약에 C++ 에서는 따라서 반면에 그 후에 이와 같이 데드락을 해결하는 것은 매우 복잡합니다 (또한 완벽하지 않지요). 애초에 데드락 상황이 발생할 수 없게 프로그램을 잘 설계하는 것이 중요합니다. C++ Concurrency In Action 이란 책에선 데드락 상황을 피하기 위해 다음과 같은 가이드라인을 제시하고 있습니다. 모든 쓰레드들이 최대 1 개의 Lock 만을 소유한다면 (일반적인 경우에) 데드락 상황이 발생하는 것을 피할 수 있습니다. 또한 대부분의 디자인에서는 1 개의 Lock 으로도 충분합니다. 만일 여러개의 Lock 을 필요로 한다면 정말 필요로 하는지 를 되물어보는 것이 좋습니다. 사실 이 가이드라인 역시 위에서 말한 내용과 자연스럽게 따라오는 것이긴 한데, 유저 코드에서 Lock 을 소유할 수 도 있기에 중첩된 Lock 을 얻는 것을 피하려면 Lock 소유시 유저 코드를 호출하는 것을 지양해야 합니다. 만일 여러개의 Lock 들을 획득해야 할 상황이 온다면, 반드시 이 Lock 들을 정해진 순서로 획득해야 합니다. 우리가 앞선 예제에서 데드락이 발생했던 이유 역시, 다음으로 멀티 쓰레드 프로그램에서 가장 많이 등장하는 생산자(producer)-소비자(consumer) 패턴에 대해서 살펴보겠습니다. 생산자는 여러분의 상사, 소비자는 바로 일을 처리하는 여러분 입니다!생산자의 경우, 무언가 처리할 일을 받아오는 쓰레드를 의미합니다. 예를 들어서, 여러분이 인터넷에서 페이지를 긁어서 분석하는 프로그램을 만들었다고 생각해봅시다. 이 경우 페이지를 긁어 오는 쓰레드가 바로 생산자가 되겠지요. 소비자의 경우, 받은 일을 처리하는 쓰레드를 의미합니다. 앞선 예제의 경우 긁어온 페이지를 분석하는 쓰레드가 해당 역할을 하겠습니다. 그렇다면 이와 같은 상황을 쓰레드로 어떻게 구현할지 살펴보겠습니다. #include <chrono> // std::chrono::miliseconds #include <iostream> #include <mutex> #include <queue> #include <string> #include <thread> #include <vector> void producer(std::queue<std::string>* downloaded_pages, std::mutex* m, int index) { for (int i = 0; i < 5; i++) { // 웹사이트를 다운로드 하는데 걸리는 시간이라 생각하면 된다. // 각 쓰레드 별로 다운로드 하는데 걸리는 시간이 다르다. std::this_thread::sleep_for(std::chrono::milliseconds(100 * index)); std::string content = "웹사이트 : " + std::to_string(i) + " from thread(" + std::to_string(index) + ")\n"; // data 는 쓰레드 사이에서 공유되므로 critical section 에 넣어야 한다. m->lock(); downloaded_pages->push(content); m->unlock(); } } void consumer(std::queue<std::string>* downloaded_pages, std::mutex* m, int* num_processed) { // 전체 처리하는 페이지 개수가 5 * 5 = 25 개. while (*num_processed < 25) { m->lock(); // 만일 현재 다운로드한 페이지가 없다면 다시 대기. if (downloaded_pages->empty()) { m->unlock(); // (Quiz) 여기서 unlock 을 안한다면 어떻게 될까요? // 10 밀리초 뒤에 다시 확인한다. std::this_thread::sleep_for(std::chrono::milliseconds(10)); continue; } // 맨 앞의 페이지를 읽고 대기 목록에서 제거한다. std::string content = downloaded_pages->front(); downloaded_pages->pop(); (*num_processed)++; m->unlock(); // content 를 처리한다. std::cout << content; std::this_thread::sleep_for(std::chrono::milliseconds(80)); } } int main() { // 현재 다운로드한 페이지들 리스트로, 아직 처리되지 않은 것들이다. std::queue<std::string> downloaded_pages; std::mutex m; std::vector<std::thread> producers; for (int i = 0; i < 5; i++) { producers.push_back(std::thread(producer, &downloaded_pages, &m, i + 1)); } int num_processed = 0; std::vector<std::thread> consumers; for (int i = 0; i < 3; i++) { consumers.push_back( std::thread(consumer, &downloaded_pages, &m, &num_processed)); } for (int i = 0; i < 5; i++) { producers[i].join(); } for (int i = 0; i < 3; i++) { consumers[i].join(); } } 성공적으로 컴파일 하였다면 실행 결과 웹사이트 : 0 from thread(1) 웹사이트 : 0 from thread(2) 웹사이트 : 1 from thread(1) 웹사이트 : 0 from thread(3) 웹사이트 : 2 from thread(1) 웹사이트 : 0 from thread(4) 웹사이트 : 1 from thread(2) 웹사이트 : 3 from thread(1) 웹사이트 : 0 from thread(5) 웹사이트 : 4 from thread(1) 웹사이트 : 1 from thread(3) 웹사이트 : 2 from thread(2) 웹사이트 : 1 from thread(4) 웹사이트 : 3 from thread(2) 웹사이트 : 2 from thread(3) 웹사이트 : 1 from thread(5) 웹사이트 : 4 from thread(2) 웹사이트 : 2 from thread(4) 웹사이트 : 3 from thread(3) 웹사이트 : 2 from thread(5) 웹사이트 : 4 from thread(3) 웹사이트 : 3 from thread(4) 웹사이트 : 3 from thread(5) 웹사이트 : 4 from thread(4) 웹사이트 : 4 from thread(5) 와 같이 나옵니다. 일단 위 코드가 어떻게 생산자-소비자 패턴을 구현하였는지 살펴봅시다. std::queue<std::string> downloaded_pages; 먼저 왜 굳이 큐를 사용하였나면 큐가 바로 먼저 들어온 것이 먼저 나간다(First In First Out - FIFO) 라는 특성이 있기 때문입니다. 쉽게 말해, 먼저 다운로드한 페이지를 먼저 처리하기 위함이지요. 물론 하지만 큐의 경우 해당 연산들이 매우 빠르게 이루어질 수 있습니다.
// 웹사이트를 다운로드 하는데 걸리는 시간이라 생각하면 된다. // 각 쓰레드 별로 다운로드 하는데 걸리는 시간이 다르다. std::this_thread::sleep_for(std::chrono::milliseconds(100 * index)); std::string content = "웹사이트 : " + std::to_string(i) + " from thread(" + std::to_string(index) + ")\n"; // downloaded_pages 는 쓰레드 사이에서 공유되므로 critical section 에 넣어야 // 한다. m->lock(); downloaded_pages->push(content); m->unlock(); 일단 기본적으로 C++ 표준 라이브러리 상에서는 인터넷 페이지를 다운받는 기능을 제공하지 않기 때문에, 대략 비슷한 상황을 가정하고 시뮬레이션 하였습니다.
그리고 다운 받은 웹사이트 내용이 그렇다면, 이제 다운 받은 페이지를 작업 큐에 집어 넣어야 합니다. 이 때 주의할 점으로, 이를 방지 하기 위해서 뮤텍스 자 그럼 먼저 우리의 따라서, 실제로는 아래와 같이 구현하였습니다. m->lock(); // 만일 현재 다운로드한 페이지가 없다면 다시 대기. if (downloaded_pages->empty()) { m->unlock(); // (Quiz) 여기서 unlock 을 안한다면 어떻게 될까요? // 10 밀리초 뒤에 다시 확인한다. std::this_thread::sleep_for(std::chrono::milliseconds(10)); continue; }
참고로 // 맨 앞의 페이지를 읽고 대기 목록에서 제거한다. std::string content = downloaded_pages->front(); downloaded_pages->pop(); (*num_processed)++; m->unlock(); // content 를 처리한다. std::cout << content; std::this_thread::sleep_for(std::chrono::milliseconds(80)); 마지막으로 이 때 우리의 위 그림 처럼 우리의 구현에서 이는 매우 비효율적입니다. 매 번 언제 올지 모르는 데이터를 확인하기 위해 지속적으로 차라리 C++ 에서는 위와 같은 형태로 생산자 소비자 패턴을 구현할 수 있도록 여러가지 도구들을 제공하고 있습니다. 위와 같은 상황에서 쓰레드들을 10 밀리초 마다 재웠다 깨웠다 할 수 밖에 없었던 이유는 어떠 어떠한 조건을 만족할 때 까지 자라! 라는 명령을 내릴 수 없었기 때문입니다. 위 경우 이는 조건 변수( #include <chrono> // std::chrono::miliseconds #include <condition_variable> // std::condition_variable #include <iostream> #include <mutex> #include <queue> #include <string> #include <thread> #include <vector> void producer(std::queue<std::string>* downloaded_pages, std::mutex* m, int index, std::condition_variable* cv) { for (int i = 0; i < 5; i++) { // 웹사이트를 다운로드 하는데 걸리는 시간이라 생각하면 된다. // 각 쓰레드 별로 다운로드 하는데 걸리는 시간이 다르다. std::this_thread::sleep_for(std::chrono::milliseconds(100 * index)); std::string content = "웹사이트 : " + std::to_string(i) + " from thread(" + std::to_string(index) + ")\n"; // data 는 쓰레드 사이에서 공유되므로 critical section 에 넣어야 한다. m->lock(); downloaded_pages->push(content); m->unlock(); // consumer 에게 content 가 준비되었음을 알린다. cv->notify_one(); } } void consumer(std::queue<std::string>* downloaded_pages, std::mutex* m, int* num_processed, std::condition_variable* cv) { while (*num_processed < 25) { std::unique_lock<std::mutex> lk(*m); cv->wait( lk, [&] { return !downloaded_pages->empty() || *num_processed == 25; }); if (*num_processed == 25) { lk.unlock(); return; } // 맨 앞의 페이지를 읽고 대기 목록에서 제거한다. std::string content = downloaded_pages->front(); downloaded_pages->pop(); (*num_processed)++; lk.unlock(); // content 를 처리한다. std::cout << content; std::this_thread::sleep_for(std::chrono::milliseconds(80)); } } int main() { // 현재 다운로드한 페이지들 리스트로, 아직 처리되지 않은 것들이다. std::queue<std::string> downloaded_pages; std::mutex m; std::condition_variable cv; std::vector<std::thread> producers; for (int i = 0; i < 5; i++) { producers.push_back( std::thread(producer, &downloaded_pages, &m, i + 1, &cv)); } int num_processed = 0; std::vector<std::thread> consumers; for (int i = 0; i < 3; i++) { consumers.push_back( std::thread(consumer, &downloaded_pages, &m, &num_processed, &cv)); } for (int i = 0; i < 5; i++) { producers[i].join(); } // 나머지 자고 있는 쓰레드들을 모두 깨운다. cv.notify_all(); for (int i = 0; i < 3; i++) { consumers[i].join(); } } 성공적으로 컴파일 하였다면 실행 결과 웹사이트 : 0 from thread(1) 웹사이트 : 0 from thread(2) 웹사이트 : 1 from thread(1) 웹사이트 : 0 from thread(3) 웹사이트 : 2 from thread(1) 웹사이트 : 1 from thread(2) 웹사이트 : 0 from thread(4) 웹사이트 : 3 from thread(1) 웹사이트 : 0 from thread(5) 웹사이트 : 4 from thread(1) 웹사이트 : 1 from thread(3) 웹사이트 : 2 from thread(2) 웹사이트 : 1 from thread(4) 웹사이트 : 3 from thread(2) 웹사이트 : 2 from thread(3) 웹사이트 : 1 from thread(5) 웹사이트 : 4 from thread(2) 웹사이트 : 2 from thread(4) 웹사이트 : 3 from thread(3) 웹사이트 : 2 from thread(5) 웹사이트 : 4 from thread(3) 웹사이트 : 3 from thread(4) 웹사이트 : 3 from thread(5) 웹사이트 : 4 from thread(4) 웹사이트 : 4 from thread(5) 와 같이 나옵니다. condition_variable cv;
먼저 뮤텍스를 정의할 때와 같이 std::unique_lock<std::mutex> lk(*m); cv->wait(lk, [&] { return !downloaded_pages->empty() || *num_processed == 25; }); 대충 코드를 보면 느낌이 오겠지만, !downloaded_pages->empty() || *num_processed == 25; 를 전달하였는데, 이는 조건 변수는 만일 해당 조건이 거짓이라면, 반면에 해당 조건이 참이라면 std::unique_lock<std::mutex> lk(*m); 참고로 기존의 덧붙여 if (*num_processed == 25) { lk.unlock(); return; }
자
그렇다면 // consumer 에게 content 가 준비되었음을 알린다. cv->notify_one(); 만약에 페이지를 하나 다운 받았다면, 잠자고 있는 쓰레드들 중 하나를 깨워서 일을 시켜야겠죠? (만약에 모든 쓰레드들이 일을 하고 있는 상태라면 아무 일도 일어나지 않습니다.) for (int i = 0; i < 5; i++) { producers[i].join(); } // 나머지 자고 있는 쓰레드들을 모두 깨운다. cv.notify_all();
따라서 마지막으로 자 그럼 이것으로 이번 강좌를 마치도록 하겠습니다. 다음 강좌에서는 C++ 에서 제공하는 또 다른 기능인 뭘 배웠지?여러 쓰레드에서 같은 객체의 값을 수정한다면 Race Condition 이 발생합니다. 이를 해결하기 위해서는 여러가지 방법이 있지만, 한 가지 방법으로 뮤텍스를 사용하는 방법이 있습니다. 뮤텍스는 한 번에 한 쓰레드에서만 획득할 수 있습니다. 획득한 뮤텍스는 반드시 반환해야 합니다.
강좌를 보다가 조금이라도 궁금한 것이나 이상한 점이 있다면 꼭 댓글을 남겨주시기 바랍니다. 그 외에도 강좌에 관련된 것이라면 어떠한 것도 질문해 주셔도 상관 없습니다. 생각해 볼 문제도 정 모르겠다면 댓글을 달아주세요. 현재 여러분이 보신 강좌는 <씹어먹는 C ++ - <15 - 2. C++ 뮤텍스(mutex) 와 조건 변수(condition variable)>> 입니다. 이번 강좌의 모든 예제들의 코드를 보지 않고 짤 수준까지 강좌를 읽어 보시기 전까지 다음 강좌로 넘어가지 말아주세요 |