1. 스레드 생성 및 관리
1.1 스레드란 무엇인가?
스레드는 프로세스 내에서 실행되는 경량 프로세스로, 독립적으로 실행될 수 있는 코드의 흐름입니다. 스레드는 같은 메모리 공간을 공유하므로 데이터 교환이 용이하지만, 각 스레드는 별도의 호출 스택과 레지스터를 가집니다. 이는 스레드가 독립적으로 실행될 수 있도록 해줍니다.
1.2 스레드의 장점과 단점
- 장점:
- 병렬 처리: 여러 작업을 동시에 처리하여 성능을 향상시킬 수 있습니다.
- 자원 공유: 같은 프로세스 내의 스레드는 메모리와 파일 디스크립터 등을 공유하므로 데이터 교환이 쉽습니다.
- 단점:
- 동기화 문제: 여러 스레드가 동일한 자원에 접근할 경우 데이터 경합이 발생할 수 있습니다.
- 디버깅 어려움: 스레드 간의 상호작용이 복잡해질수록 디버깅이 어려워집니다.
1.3 C++에서 스레드 생성하기
C++11부터 <thread>
헤더를 사용하여 스레드를 생성할 수 있습니다. 아래는 간단한 스레드 생성 예제입니다.
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
// 새 스레드 생성
std::thread t(hello);
// 메인 스레드가 종료되기 전에 t가 완료되도록 대기
t.join();
return 0;
}
std::thread t(hello)
:hello
함수를 새로운 스레드에서 실행합니다.t.join()
: 메인 스레드가t
스레드가 종료될 때까지 기다립니다. 이를 통해 스레드의 실행이 완료될 때까지 프로그램이 종료되지 않도록 합니다.
1.4 여러 개의 스레드 생성하기
여러 작업을 동시에 수행하려면 여러 개의 스레드를 생성할 수 있습니다.
#include <iostream>
#include <thread>
#include <vector>
void print_id(int id) {
std::cout << "Thread ID: " << id << std::endl;
}
int main() {
const int num_threads = 5; // 사용할 스레드 개수
std::vector<std::thread> threads;
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back(print_id, i); // 각 스레드에 고유한 ID 전달
}
for (auto& th : threads) {
th.join(); // 모든 스레드가 끝날 때까지 대기
}
return 0;
}
threads.emplace_back(print_id, i)
:print_id
함수를 각 스레드에서 실행하며, 고유한 ID를 전달합니다.th.join()
: 모든 스레드가 종료될 때까지 메인 스레드가 대기합니다.
1.5 스레드의 생명주기 관리
스레드는 생성, 실행, 종료의 세 단계를 거칩니다. 스레드가 종료되면 해당 스레드의 리소스는 해제됩니다. 그러나 스레드가 종료되지 않은 상태에서 프로그램이 종료되면 리소스 누수가 발생할 수 있으므로, join()
또는 detach()
를 통해 스레드의 생명주기를 관리해야 합니다.
join()
: 스레드가 종료될 때까지 기다립니다.detach()
: 스레드를 독립적으로 실행시킵니다. 이 경우 스레드는 메인 스레드와 별개로 실행됩니다.
예제: detach()
사용하기
#include <iostream>
#include <thread>
#include <chrono>
void background_task() {
for (int i = 0; i < 5; ++i) {
std::cout << "Background task is running... " << i << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main() {
std::thread t(background_task);
t.detach(); // 스레드를 독립적으로 실행
std::cout << "Main thread continues to run..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3)); // 메인 스레드 대기
return 0;
}
t.detach()
: 스레드를 독립적으로 실행시킵니다. 메인 스레드는t
스레드의 종료를 기다리지 않습니다.
2. 뮤텍스와 잠금
2.1 뮤텍스란 무엇인가?
뮤텍스(Mutex)는 "Mutual Exclusion"의 약자로, 한 번에 하나의 스레드만 특정 코드 블록이나 자원에 접근하도록 제한하는 도구입니다. 이를 통해 데이터 경합(data race)을 방지할 수 있습니다.
2.2 뮤텍스 사용 예제
아래는 뮤텍스를 사용하여 공유 자원을 보호하는 예제입니다.
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 뮤텍스 객체 선언
int shared_counter = 0;
void increment_counter(int id) {
mtx.lock(); // 뮤텍스를 잠금으로써 보호 시작
++shared_counter;
std::cout << "Thread " << id << ": Counter = " << shared_counter << std::endl;
mtx.unlock(); // 뮤텍스를 해제하여 다른 스레드가 접근 가능하도록 함
}
int main() {
const int num_threads = 10;
std::vector<std::thread> threads;
for (int i = 0; i < num_threads; ++i) {
threads.emplace_back(increment_counter, i);
}
for (auto& th : threads) {
th.join();
}
return 0;
}
mtx.lock()
: 뮤텍스를 잠급니다. 다른 스레드는 이 코드 블록에 접근할 수 없습니다.mtx.unlock()
: 뮤텍스를 해제합니다. 다른 스레드가 접근할 수 있도록 합니다.
2.3 RAII 패턴과 std::lock_guard
C++에서는 RAII(Resource Acquisition Is Initialization) 패턴을 사용하여 뮤텍스를 더 안전하게 관리할 수 있습니다. std::lock_guard
는 생성자에서 뮤텍스를 잠그고 소멸자에서 자동으로 해제합니다.
void increment_counter(int id) {
std::lock_guard<std::mutex> lock(mtx); // 자동으로 락이 걸리고 해제됨
++shared_counter;
std::cout << "Thread " << id << ": Counter = " << shared_counter << std::endl;
}
std::lock_guard<std::mutex> lock(mtx)
: 뮤텍스를 자동으로 잠그고, 함수가 종료될 때 자동으로 해제합니다.
2.4 std::unique_lock
의 활용
std::unique_lock
은 std::lock_guard
보다 더 유연한 뮤텍스 관리 기능을 제공합니다. 예를 들어, 뮤텍스를 수동으로 잠그고 해제할 수 있으며, 조건 변수와 함께 사용할 때 유용합니다.
void increment_counter(int id) {
std::unique_lock<std::mutex> lock(mtx);
++shared_counter;
std::cout << "Thread " << id << ": Counter = " << shared_counter << std::endl;
lock.unlock(); // 수동으로 뮤텍스 해제
}
2.5 데드락(Deadlock) 방지
데드락은 두 개 이상의 스레드가 서로의 자원을 기다리며 무한히 대기하는 상태를 말합니다. 데드락을 방지하기 위해서는 다음과 같은 전략을 사용할 수 있습니다:
- 락 순서 정하기: 모든 스레드가 같은 순서로 락을 획득하도록 합니다.
- 타임아웃 설정: 락을 획득할 때 타임아웃을 설정하여 무한 대기를 방지합니다.
- 락 계층 구조 사용: 락의 계층 구조를 정의하여 데드락 가능성을 줄입니다.
예제: 데드락 방지
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1;
std::mutex mtx2;
void task1() {
std::lock(mtx1, mtx2); // 동시에 두 뮤텍스를 잠금
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
std::cout << "Task 1 is running..." << std::endl;
}
void task2() {
std::lock(mtx1, mtx2); // 동시에 두 뮤텍스를 잠금
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
std::cout << "Task 2 is running..." << std::endl;
}
int main() {
std::thread t1(task1);
std::thread t2(task2);
t1.join();
t2.join();
return 0;
}
std::lock(mtx1, mtx2)
: 두 뮤텍스를 동시에 잠급니다. 이를 통해 데드락을 방지할 수 있습니다.
3. 조건 변수
3.1 조건 변수란 무엇인가?
조건 변수(Condition Variable)는 특정 조건이 충족될 때까지 스레드를 대기시키고, 다른 스레드가 그 조건을 만족시켰을 때 기다리고 있는 스레드를 깨울 수 있도록 도와주는 동기화 도구입니다.
3.2 조건 변수 사용 예제
아래는 생산자-소비자 문제를 해결하기 위해 조건 변수를 사용하는 예제입니다.
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::queue<int> buffer;
const unsigned int maxBufferSize = 5;
std::mutex mtx;
std::condition_variable cv;
void producer() {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
// 버퍼가 가득 찼다면 대기
cv.wait(lock, [] { return buffer.size() < maxBufferSize; });
// 아이템 추가
buffer.push(i);
std::cout << "Produced: " << i << "\n";
// 소비자에게 신호 보내기
cv.notify_all();
}
}
void consumer() {
for (int i = 0; i < 10; ++i) {
std::unique_lock<std::mutex> lock(mtx);
// 버퍼가 비어있다면 대기
cv.wait(lock, [] { return !buffer.empty(); });
// 아이템 제거
int item = buffer.front();
buffer.pop();
std::cout << "Consumed: " << item << "\n";
// 생산자에게 신호 보내기
cv.notify_all();
}
}
int main() {
std::thread prodThread(producer);
std::thread consThread(consumer);
prodThread.join();
consThread.join();
return 0;
}
cv.wait(lock, 조건)
: 조건이 충족될 때까지 스레드를 대기시킵니다.cv.notify_all()
: 대기 중인 모든 스레드에게 신호를 보냅니다.
3.3 조건 변수의 활용 사례
조건 변수는 주로 생산자-소비자 패턴, 스레드 풀, 이벤트 기반 프로그래밍 등에서 사용됩니다. 예를 들어, 작업 큐에 작업이 추가될 때까지 스레드를 대기시키고, 작업이 추가되면 스레드를 깨워 작업을 처리하는 방식으로 활용할 수 있습니다.
3.4 조건 변수의 고급 사용법
조건 변수는 복잡한 동기화 문제를 해결하는 데 매우 유용합니다. 예를 들어, 여러 조건을 동시에 확인하거나, 특정 조건이 충족될 때까지 스레드를 대기시키는 등의 고급 기법을 사용할 수 있습니다.
void complex_condition_example() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] {
return buffer.size() < maxBufferSize && some_other_condition();
});
// 복잡한 조건을 만족할 때까지 대기
}
결론
C++에서 멀티스레딩은 성능 최적화와 효율성을 높이는 강력한 도구입니다. 스레드 생성, 뮤텍스를 통한 동기화, 조건 변수를 활용한 스레드 간 통신을 통해 복잡한 동시성 문제를 해결할 수 있습니다. 이러한 기법들을 적절히 활용하면 더욱 안정적이고 효율적인 프로그램을 작성할 수 있습니다. 추가적으로, 스레드 풀, 비동기 프로그래밍, 병렬 알고리즘 등 고급 주제로 나아가면 더욱 강력한 멀티스레딩 애플리케이션을 개발할 수 있습니다. 멀티스레딩은 복잡하지만, 그만큼 강력한 도구이므로 꾸준히 학습하고 실습해보는 것이 중요합니다.
'프로그래밍 > C++' 카테고리의 다른 글
C++ 네임스페이스와 모듈: 코드 구조화와 관리의 핵심 (0) | 2025.02.03 |
---|---|
고급 객체 지향 프로그래밍: 다형성, 가상 함수, 추상 클래스 (0) | 2025.02.03 |
현대 C++의 강력한 기능: 람다 표현식, auto 키워드, 그리고 범위 기반 for 루프 (0) | 2025.02.03 |
C++ 스마트 포인터: 안전하고 효율적인 메모리 관리 (0) | 2025.02.03 |
C++ 표준 템플릿 라이브러리(STL): 컨테이너, 반복자, 알고리즘 종합 가이드 (0) | 2025.02.03 |