반응형
생산자 - 소비자 패턴으로 스레드 공부하기
최근에 스레드에 대해서 배움이 부족한 것을 느껴서 스레드에 대해서 공부하고 있다. 스레드를 공부할때 가장 기본이 되는 생산자 소비자 패턴에 대해서 알아보자.
예제 코드
해당 예제에서는 chef()와 waiter() 함수가 존재하는데 해당 함수를 스레드를 생성해서 동작시킨다. chef()는 음식을 계속 만들고 waiter()는 order 큐에 음식이 들어있으면 이를 서빙한다. 완벽한 생산자와 소비자의 모습을 보여준다. 그리고 order 큐에 더 이상 음식이 없고 영업이 종료되었다면 모두가 퇴근하게 된다.
주목해야할 점은 std::condition_variable을 사용해서 wait를 통해서 잠에 들고 notify_one을 사용해서 깨우는 구조이다. 만약 std::condition_variable을 사용하지 않는다면 바쁜 대기를 사용해서 주기적으로 확인해야만 한다.
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <queue>
#include <string>
std::mutex mtx; // 공유 자원(주문 목록)을 보호하는 열쇠
std::condition_variable cv; // 요리사와 서빙 직원이 소통하는 벨 신호기
std::queue<std::string> orders; // 요리사가 만든 음식을 담는 접시 (공유 자원)
bool isFinish = false; // 영업 종료 여부
void chef()
{
for (int i = 0; i <= 10; ++i)
{
std::this_thread::sleep_for(std::chrono::seconds(1)); // 요리 중...
std::string food = "food #" + std::to_string(i);
{
std::lock_guard<std::mutex> lock(mtx);
orders.push(food);
std::cout << "[요리사] " << food << " 완성! \n";
}
cv.notify_one();
}
{
std::lock_guard<std::mutex> lock(mtx);
isFinish = true;
}
cv.notify_one(); // 마지막 종료 신호
}
void waiter()
{
while (true)
{
std::string food;
{
std::unique_lock<std::mutex> lock(mtx); // 1. 일단 열쇠를 집어듬
// 2. 음식이 없다면 잠에 든다.
cv.wait(lock, [] { return !orders.empty() || isFinish; });
if (orders.empty() && isFinish) break;
food = orders.front();
orders.pop();
}
std::cout << "[서빙 #" << std::this_thread::get_id() << "] :" << food << " 서빙중 \n";
std::this_thread::sleep_for(std::chrono::seconds(3));
}
std::cout << "[서빙 #" << std::this_thread::get_id() << "] 모든 업무 종료.퇴근합니다!\n";
}
int main()
{
std::cout << "show begin--\n";
std::thread t1(chef);
std::thread t2(waiter);
std::thread t3(waiter);
std::thread t4(waiter);
t1.join();
t2.join();
t3.join();
t4.join();
return 0;
}
std::condition_variable 없이 구현한 코드
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <queue>
#include <string>
std::mutex mtx;
std::queue<std::string> orders;
bool isFinish = false;
void chef()
{
for (int i = 0; i <= 10; ++i)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::string food = "food #" + std::to_string(i);
{
std::lock_guard<std::mutex> lock(mtx);
orders.push(food);
std::cout << "[요리사] " << food << " 완성! \n";
}
// notify_one()이 사라짐: 신호를 보낼 필요가 없음
}
{
std::lock_guard<std::mutex> lock(mtx);
isFinish = true;
}
}
void waiter()
{
while (true)
{
std::string food = "";
{
std::lock_guard<std::mutex> lock(mtx); // 매번 락을 걸고 확인
if (!orders.empty())
{
food = orders.front();
orders.pop();
}
else if (isFinish)
{
break; // 음식 없고 영업 종료면 퇴근
}
}
if (food != "")
{
std::cout << "[서빙 #" << std::this_thread::get_id() << "] :" << food << " 서빙중 \n";
std::this_thread::sleep_for(std::chrono::seconds(3));
}
else
{
// 중요: 락을 풀고 아주 잠깐 쉽니다. 안 쉬면 CPU 점유율이 100%가 됩니다.
std::this_thread::yield();
}
}
std::cout << "[서빙 #" << std::this_thread::get_id() << "] 퇴근합니다!\n";
}
int main()
{
std::thread t1(chef);
std::thread t2(waiter);
std::thread t3(waiter);
std::thread t4(waiter);
t1.join();
t2.join();
t3.join();
t4.join();
return 0;
}
정리하자면
- condition_variable
- CPU 효율 매우 높음 (대기하는 동안 CPU 거의 안씀)
- 신호 받는 즉시 깨어남
- 상대적으로 로직이 복잡해짐(wait, notify 이해 필요)
- 바쁜 대기(Busy Waiting)
- CPU 효율 낮음 (계속 확인하느라고 CPU 계속 사용)
- 체크 주기에 따라 미세한 지연
- 직관적이고 단순함
'프로그래밍 > C++' 카테고리의 다른 글
| 바쁜 대기(Busy Waiting)에 대해서 알아보자 (0) | 2026.02.01 |
|---|---|
| 상호배제란 무엇인가? (0) | 2026.02.01 |
| thread_local 키워드에 대해서 (0) | 2026.01.31 |
| RAII에 대해서 알아보자 (0) | 2026.01.26 |
| C++ 임시 객체에 대해서 (0) | 2026.01.25 |
댓글