C++에서 동기화(Synchronization)는 멀티스레드 환경에서 여러 스레드가 동시에 공유 데이터에 접근할 때 발생할 수 있는 경쟁 상태(Race Condition)를 방지하고, 데이터 무결성을 유지하기 위해 사용됩니다. C++에서 제공하는 주요 동기화 기법들을 설명하겠습니다.
1. 뮤텍스 (Mutex, Mutual Exclusion)
뮤텍스는 한 번에 하나의 스레드만 특정 코드 블록을 실행할 수 있도록 보장하는 동기화 객체입니다.
사용 예시 (std::mutex)
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 뮤텍스 객체
int shared_data = 0;
void increment() {
std::lock_guard<std::mutex> lock(mtx); // 뮤텍스 잠금 (자동 해제됨)
++shared_data;
std::cout << "Shared Data: " << shared_data << std::endl;
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
return 0;
}
설명
- std::mutex를 사용하여 공유 데이터(shared_data)를 보호합니다.
- std::lock_guard<std::mutex>는 뮤텍스를 자동으로 잠그고, 블록이 끝나면 자동으로 해제합니다.
2. 재귀적 뮤텍스 (std::recursive_mutex)
재귀적 뮤텍스는 같은 스레드가 여러 번 같은 뮤텍스를 잠글 수 있도록 허용합니다.
#include <iostream>
#include <thread>
#include <mutex>
std::recursive_mutex rmtx;
void recursiveFunction(int n) {
if (n == 0) return;
rmtx.lock();
std::cout << "Recursive call: " << n << std::endl;
recursiveFunction(n - 1);
rmtx.unlock();
}
int main() {
std::thread t1(recursiveFunction, 3);
t1.join();
return 0;
}
설명
- std::mutex는 같은 스레드가 다시 잠그려고 하면 데드락(Deadlock)이 발생하지만, std::recursive_mutex는 이를 허용합니다.
- 하지만 과도한 사용은 성능 저하를 초래할 수 있습니다.
3. 조건 변수 (std::condition_variable)
조건 변수는 특정 조건이 만족될 때까지 스레드를 대기 상태로 만들고, 특정 이벤트가 발생하면 다시 실행되도록 합니다.
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; }); // ready가 true가 될 때까지 대기
std::cout << "Worker thread is running!" << std::endl;
}
int main() {
std::thread t(worker);
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one(); // 하나의 스레드를 깨움
t.join();
return 0;
}
설명
- cv.wait()를 통해 스레드가 특정 조건(ready)이 만족될 때까지 대기하도록 합니다.
- cv.notify_one()은 하나의 스레드를 깨우고, cv.notify_all()은 모든 대기 중인 스레드를 깨웁니다.
4. 읽기-쓰기 락 (std::shared_mutex)
여러 개의 스레드가 동시에 읽기(read) 작업을 수행할 수 있지만, 쓰기(write) 작업이 진행될 때는 하나의 스레드만 접근할 수 있도록 하는 동기화 기법입니다.
#include <iostream>
#include <thread>
#include <shared_mutex>
std::shared_mutex rw_mutex;
int shared_data = 0;
void reader() {
std::shared_lock<std::shared_mutex> lock(rw_mutex);
std::cout << "Reader thread: " << shared_data << std::endl;
}
void writer() {
std::unique_lock<std::shared_mutex> lock(rw_mutex);
++shared_data;
std::cout << "Writer thread updated shared_data to " << shared_data << std::endl;
}
int main() {
std::thread t1(reader);
std::thread t2(writer);
std::thread t3(reader);
t1.join();
t2.join();
t3.join();
return 0;
}
설명
- std::shared_lock<std::shared_mutex>를 사용하면 여러 개의 읽기 스레드가 동시에 실행됩니다.
- std::unique_lock<std::shared_mutex>는 쓰기 작업을 보호하며, 쓰기 작업이 진행될 때는 다른 스레드가 접근할 수 없습니다.
5. atomic 변수 (std::atomic)
std::atomic을 사용하면 락 없이도 원자적인(Atomic) 연산을 수행할 수 있습니다.
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> shared_data(0);
void increment() {
shared_data.fetch_add(1, std::memory_order_relaxed);
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Final shared_data: " << shared_data.load() << std::endl;
return 0;
}
설명
- std::atomic<int>을 사용하여 락 없이도 스레드 안전하게 변수를 증가시킬 수 있습니다.
- fetch_add()는 원자적으로 증가 연산을 수행합니다.
- 메모리 순서(std::memory_order_relaxed, std::memory_order_seq_cst 등)를 조절하여 성능을 최적화할 수 있습니다.
6. Future, Promise (std::future & std::promise)
스레드 간 데이터 전달과 비동기 작업 결과를 가져오는 데 사용됩니다.
#include <iostream>
#include <thread>
#include <future>
int calculate() {
return 42;
}
int main() {
std::future<int> result = std::async(std::launch::async, calculate);
std::cout << "Result: " << result.get() << std::endl;
return 0;
}
설명
- std::async를 사용하면 비동기 작업을 수행하고 결과를 가져올 수 있습니다.
- std::future::get()을 호출하면 결과가 준비될 때까지 대기합니다.
7. 세마포어(std::counting_semaphore, std::binary_semaphore)
뮤텍스(mutex)와 비슷하지만 차이점이 있으며, 여러 개의 스레드가 동시 접근 가능한 리소스 개수를 조절하는 데 유용합니다. C++20부터 std::counting_semaphore가 표준 라이브러리에 포함되었습니다.
#include <iostream>
#include <thread>
#include <semaphore>
std::counting_semaphore<2> sem(2); // 동시에 2개의 스레드만 접근 가능
void worker(int id) {
sem.acquire(); // 세마포어 획득 (카운트 감소)
std::cout << "Thread " << id << " is working...\n";
std::this_thread::sleep_for(std::chrono::seconds(1)); // 작업 수행
std::cout << "Thread " << id << " finished.\n";
sem.release(); // 세마포어 해제 (카운트 증가)
}
int main() {
std::thread t1(worker, 1);
std::thread t2(worker, 2);
std::thread t3(worker, 3);
std::thread t4(worker, 4);
t1.join();
t2.join();
t3.join();
t4.join();
return 0;
}
설명
- acquire() 호출 시 세마포어 카운트가 감소.
- release() 호출 시 세마포어 카운트가 증가.
- binary_semaphore 는 count가 0 또는 1인 세마포어
결론
C++에서 제공하는 다양한 동기화 기법을 사용하면 멀티스레드 환경에서 안전한 프로그래밍이 가능합니다.
어떤 기법을 사용할지는 상황에 따라 다르지만, 일반적인 가이드라인은 다음과 같습니다.
종류 | 사용 |
뮤텍스(std::mutex) | 공유 데이터 보호용으로 가장 많이 사용됨. |
조건 변수(std::condition_variable) | 특정 이벤트를 대기하는 경우 사용. |
읽기-쓰기 락(std::shared_mutex) | 읽기 성능이 중요한 경우. |
원자 변수(std::atomic) | 단순한 변수 증가/감소 연산 시 성능 최적화. |
퓨처와 프로미스(std::future & std::promise) | 비동기 작업을 관리할 때 유용. |
세마포어(std::counting_semaphore와 std::binary_semaphore) | 여러 스레드가 접근 가능한 리소스를 제한 |
어떤 동기화 기법이 가장 적합할지 고민하며 사용하면, 성능과 안전성을 모두 확보할 수 있습니다! 🚀
reference
https://en.cppreference.com/w/cpp/thread
Concurrency support library (since C++11) - cppreference.com
Concurrency support library C++ includes built-in support for threads, atomic operations, mutual exclusion, condition variables, and futures. [edit] Threads Threads enable programs to execute across several processor cores. manages a separate thread (class
en.cppreference.com
'프로그래밍 > C,C++' 카테고리의 다른 글
[Modern C++] STL 자료구조 (0) | 2025.03.24 |
---|---|
[Modern C++] std::atomic (1) | 2025.03.21 |
[Modern C++] std::thread (0) | 2025.03.11 |
[Modern C++] smart pointer(unique_ptr, shared_ptr, weak_ptr) (0) | 2025.03.10 |
[Modern C++] 우측값 레퍼런스 (0) | 2025.02.24 |