프로그래밍/C,C++

[Modern C++] 동기화 기법(Synchronization)

GONII 2025. 3. 20. 15:41

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