스레드와 동기화
1. std::thread, std::mutex, std::lock_guard
C++11에서는 병렬 프로그래밍을 지원하기 위해 std::thread
, std::mutex
, std::lock_guard
와 같은 다양한 도구들을 표준 라이브러리에 포함시켰습니다. 이들은 멀티스레드 환경에서 안정적인 데이터 처리를 가능하게 하며, 자원 경쟁(Race Condition)을 방지하는 데 중요한 역할을 합니다.
1.1. std::thread
std::thread
는 독립적으로 실행되는 코드 단위를 생성하는 데 사용됩니다. 하나의 프로세스 내에서 여러 개의 스레드를 생성하여, 각기 다른 작업을 동시에 처리할 수 있게 해줍니다. 이를 통해 프로그램의 반응성을 높이고, 병렬 처리를 통해 성능을 향상시킬 수 있습니다.
#include <iostream>
#include <thread>
void printMessage() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(printMessage); // 스레드 생성
t.join(); // 스레드가 종료될 때까지 기다림
return 0;
}
스레드를 생성한 후에는 join()
이나 detach()
를 호출해야 합니다. join()
은 해당 스레드가 종료될 때까지 기다리는 반면, detach()
는 스레드를 백그라운드에서 독립적으로 실행하도록 분리합니다.
1.2. std::mutex와 std::lock_guard
std::mutex
는 스레드 간 동기화를 위해 사용되는 객체입니다. 여러 스레드가 동시에 하나의 자원에 접근할 경우 충돌이 발생할 수 있는데, 이를 방지하기 위해 뮤텍스를 사용합니다. std::lock_guard
는 뮤텍스를 RAII 방식으로 관리하며, 블록을 벗어나면 자동으로 잠금을 해제해줍니다.
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void printMessage(int id) {
std::lock_guard<std::mutex> guard(mtx);
std::cout << "Thread " << id << " is printing." << std::endl;
}
int main() {
std::thread t1(printMessage, 1);
std::thread t2(printMessage, 2);
t1.join();
t2.join();
return 0;
}
위 예제에서는 두 개의 스레드가 같은 함수 printMessage
를 호출하지만, std::mutex
를 통해 동기화되어 콘솔 출력이 충돌하지 않습니다.
lock_guard
사용이 권장됩니다.
2. 병렬 알고리즘 (C++17 std::execution)
C++17에서는 <execution>
헤더를 통해 표준 라이브러리 알고리즘에 병렬 실행 정책을 적용할 수 있게 되었습니다. 이는 기존의 순차적 알고리즘(std::for_each
, std::sort
등)에 병렬 처리를 적용하여 더 빠른 결과를 기대할 수 있습니다.
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
std::for_each(std::execution::par, vec.begin(), vec.end(), [](int& n) {
n *= 2;
});
for (int n : vec) {
std::cout << n << " ";
}
return 0;
}
위 코드에서 std::execution::par
은 병렬 실행 정책을 의미하며, vec
의 모든 요소를 병렬로 처리하여 성능을 높입니다. 단, 병렬 처리 시에는 공유 자원 접근에 주의해야 하며, 알고리즘 내부에 상태 변화가 없는 경우에만 사용하는 것이 안전합니다.
3. 원자 연산 (std::atomic)
std::atomic
은 멀티스레딩 환경에서 동기화 없이 안전하게 정수나 포인터와 같은 간단한 자료형을 사용할 수 있도록 합니다. 내부적으로는 원자적(atomic) 연산을 보장하여, 여러 스레드가 동시에 값을 읽거나 쓸 때 경합 상태 없이 안전하게 동작합니다.
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> counter(0);
void incrementCounter() {
for (int i = 0; i < 1000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::thread t1(incrementCounter);
std::thread t2(incrementCounter);
t1.join();
t2.join();
std::cout << "Final counter value: " << counter.load() << std::endl;
return 0;
}
위 예제는 counter
를 std::atomic
으로 선언하고, 두 스레드가 동시에 증가시켜도 정확히 2000이 출력됩니다. 이는 fetch_add()
연산이 원자적으로 동작하기 때문입니다.
std::memory_order
는 메모리 일관성을 제어하는데 사용됩니다. 일반적인 경우 memory_order_relaxed
가 성능상 유리하지만, 복잡한 동기화에는 acquire/release
를 고려해야 합니다.
결론적으로, C++는 std::thread와 mutex, atomic 등의 도구를 통해 강력한 멀티스레딩 프로그래밍을 지원합니다. 병렬 알고리즘은 복잡한 동기화 없이도 고성능 프로그램을 작성하는 데 매우 유용하며, 정확한 이해와 활용이 필요합니다.