반응형

std::thread 사용법 정리 (C++17 기준)

1. 기본 개념

  • C++11부터 도입된 std::thread는 병렬 실행을 위한 클래스입니다.
  • 쓰레드를 생성하면 자동으로 시작되며, 작업이 끝날 때까지 join() 또는 detach() 호출로 관리해야 합니다.

2. 기본 예제

#include <iostream>
#include <thread>

void say_hello() {
    std::cout << "Hello from thread!" << std::endl;
}

int main() {
    std::thread t(say_hello); // 쓰레드 시작
    t.join();                 // 메인 쓰레드가 t를 기다림
}
  • join()이 없으면 프로그램이 끝나기 전에 쓰레드가 종료되지 않을 수 있어 런타임 오류 발생.

3. 인자 전달

void print_sum(int a, int b) {
    std::cout << a + b << std::endl;
}

int main() {
    std::thread t(print_sum, 3, 4);
    t.join();
}
  • 값은 복사되어 전달됩니다.
  • 참조를 넘기려면 std::ref() 사용
void modify(int& x) {
    x += 10;
}

int main() {
    int value = 5;
    std::thread t(modify, std::ref(value));
    t.join();
    std::cout << value << std::endl; // 15
}

4. 람다 함수 사용

std::thread t([](){
    std::cout << "Running in a lambda thread!" << std::endl;
});
t.join();

5. 멤버 함수 호출

class Worker {
public:
    void run() {
        std::cout << "Worker running!" << std::endl;
    }
};

int main() {
    Worker w;
    std::thread t(&Worker::run, &w); // 객체 포인터 전달
    t.join();
}

6. join() vs detach()

  • join(): 쓰레드가 끝날 때까지 기다림
  • detach(): 쓰레드를 백그라운드에서 실행시키고 제어를 넘김
std::thread t([]() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Detached thread done" << std::endl;
});
t.detach(); // join하지 않아도 됨

주의: detach한 쓰레드는 더 이상 제어할 수 없으며, 메인 함수가 먼저 종료되면 문제가 생길 수 있음.


7. 쓰레드 ID 확인

 
std::cout << std::this_thread::get_id() << std::endl;

8. 쓰레드 관련 함수들

  • std::this_thread::sleep_for(duration): 일정 시간 동안 sleep
  • std::this_thread::yield(): 다른 쓰레드에게 CPU 양보
  • std::thread::hardware_concurrency(): 시스템의 논리 코어 수 반환

9. 예외 처리

std::thread 자체는 예외를 전달하지 않기 때문에, 예외는 쓰레드 내부에서 처리해야 합니다.

std::thread t([]() {
    try {
        throw std::runtime_error("error!");
    } catch (const std::exception& e) {
        std::cout << "Caught exception: " << e.what() << std::endl;
    }
});
t.join();

10. C++17에서 유용한 추가 기능

  • std::scoped_lock을 쓰레드와 뮤텍스에 함께 사용 가능
  • std::shared_mutex (읽기/쓰기 락)과 함께 사용 시 성능 최적화 가능
  • 쓰레드 안전한 std::atomic 변수와 병행 사용

 

std::shared_mutex란?

  • 동시에 여러 개의 쓰레드가 읽을 수 있도록 허용하지만,
  • 쓰기(수정) 시에는 단 하나의 쓰레드만 접근 가능하게 만드는 락입니다.

읽기: 병렬 허용
쓰기: 단독만 허용

예: std::mutex와 std::shared_mutex의 차이

일반 std::mutex 사용 시

cpp
std::mutex mtx; void read() {
std::lock_guard<std::mutex> lock(mtx); // 읽기 작업
}

읽기조차 직렬화됨 (읽기끼리도 동시에 접근 불가)


std::shared_mutex 사용 시

#include <shared_mutex>

std::shared_mutex shared_mtx;

void read() {
    std::shared_lock<std::shared_mutex> lock(shared_mtx); // 여러 쓰레드 동시에 가능
    // 읽기 작업
}

void write() {
    std::unique_lock<std::shared_mutex> lock(shared_mtx); // 단독 접근
    // 쓰기 작업
}

여러 쓰레드가 동시에 읽기 가능
쓰기 쓰레드는 단독 접근만 허용 (읽기 중인 쓰레드가 끝날 때까지 대기)

성능 최적화의 이유

쓰기보다 읽기가 많은 상황 (예: 캐시, 설정 조회 등)에서는:

  • std::mutex: 모든 작업이 직렬 → 비효율적
  • std::shared_mutex: 읽기 병렬화 가능 → 성능 향상
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>

std::shared_mutex shared_mtx;
int shared_data = 0;

void reader(int id) {
    std::shared_lock<std::shared_mutex> lock(shared_mtx);
    std::cout << "Reader " << id << " read value: " << shared_data << std::endl;
}

void writer(int value) {
    std::unique_lock<std::shared_mutex> lock(shared_mtx);
    shared_data = value;
    std::cout << "Writer wrote value: " << value << std::endl;
}

int main() {
    std::vector<std::thread> threads;

    // 읽기 쓰레드 여러 개
    for (int i = 0; i < 5; ++i)
        threads.emplace_back(reader, i);

    // 쓰기 쓰레드 하나
    threads.emplace_back(writer, 100);

    for (auto& t : threads) t.join();
}

'Develop > C&CPP' 카테고리의 다른 글

[cpp] atomic  (0) 2025.04.20
[cpp] mutex, lock  (0) 2025.04.20
[cpp] cpp17에서 달라진 점  (0) 2025.04.20
[cpp] cpp14에서 추가된 것  (0) 2025.04.20
[service] 윈도우 서비스 프로그램  (0) 2025.01.05
반응형

std::atomic 개념과 사용법 (C++17 기준)

1. std::atomic이란?

  • 동기화된 접근을 보장하는 데이터 타입 템플릿입니다.
  • 멀티스레드에서 **뮤텍스 없이도 변수 접근의 원자성(atomicity)**을 보장합니다.
  • std::atomic<int>, std::atomic<bool>, std::atomic<T> 등 다양한 타입으로 사용 가능.

2. 기본 사용법

#include <atomic>
#include <iostream>
#include <thread>

std::atomic<int> counter{0};

void increment() {
    for (int i = 0; i < 10000; ++i) {
        counter++;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Final counter: " << counter << std::endl;
}
  • counter++는 원자적으로 수행됨 → 경쟁 조건(race condition) 없이 동작

3. 주요 연산

  • store(val): 값 설정
  • load(): 현재 값 반환
  • exchange(val): 기존 값을 새로운 값으로 바꾸고, 이전 값을 반환
  • compare_exchange_strong(expected, desired)
  • compare_exchange_weak(expected, desired): CAS(compare-and-swap) 연산
std::atomic<int> value{5};
int expected = 5;
bool success = value.compare_exchange_strong(expected, 10);
// 성공 시 value는 10이 되고 true 반환
 

4. 메모리 순서 (memory ordering)

  • 기본은 memory_order_seq_cst (가장 강력하고 안전한 순서)
  • 필요에 따라 memory_order_relaxed, acquire, release 등 지정 가능
counter.fetch_add(1, std::memory_order_relaxed); // 약한 메모리 순서

하지만 대부분의 경우 기본 순서를 사용하는 것으로 충분하며, 성능 최적화가 필요한 경우만 조절합니다.


5. std::atomic_flag (가장 가벼운 락)

  • 간단한 락 구현이나 spin lock 등에 사용
  • 초기화는 반드시 ATOMIC_FLAG_INIT로
std::atomic_flag flag = ATOMIC_FLAG_INIT;

void work() {
    while (flag.test_and_set(std::memory_order_acquire)) {
        // busy-wait
    }
    // critical section
    flag.clear(std::memory_order_release);
}

6. 뮤텍스 vs atomic

항목  mutex atomic
동기화 범위 임의의 코드 블록 변수 단위
오버헤드 상대적으로 큼 작음
복잡한 동작 가능 제한적
예외 안전성 RAII로 안전 직접 관리 필요

7. C++17에서의 특징

  • 템플릿 deduction을 사용할 수 없지만, std::atomic<T>의 인터페이스는 보다 풍부해졌습니다.
  • std::atomic<std::shared_ptr<T>>도 제공되어, 참조 카운팅이 원자적으로 가능해졌습니다.

'Develop > C&CPP' 카테고리의 다른 글

[cpp] thread  (0) 2025.04.20
[cpp] mutex, lock  (0) 2025.04.20
[cpp] cpp17에서 달라진 점  (0) 2025.04.20
[cpp] cpp14에서 추가된 것  (0) 2025.04.20
[service] 윈도우 서비스 프로그램  (0) 2025.01.05
반응형

C++17에서의 mutex, lock

1. 기본 std::mutex 사용

std::mutex는 임계 구역(critical section)을 보호하기 위한 가장 기본적인 뮤텍스입니다.

#include <iostream>
#include <mutex>
#include <thread>

std::mutex mtx;

void print_message(const std::string& msg) {
    mtx.lock();
    std::cout << msg << std::endl;
    mtx.unlock();
}

int main() {
    std::thread t1(print_message, "Hello from thread 1");
    std::thread t2(print_message, "Hello from thread 2");

    t1.join();
    t2.join();
    return 0;
}
 

주의: lock()과 unlock()은 반드시 쌍으로 호출되어야 하며, 예외 발생 시 unlock이 호출되지 않아 데드락의 원인이 될 수 있음.


2. std::lock_guard 사용

std::lock_guard는 예외 안전성을 위해 사용되는 RAII 스타일의 뮤텍스 관리 도구입니다.

void print_message(const std::string& msg) {
    std::lock_guard<std::mutex> lock(mtx);
    std::cout << msg << std::endl;
}

스코프를 벗어나면 자동으로 unlock()이 호출됨.


3. std::unique_lock 사용

std::unique_lock은 lock_guard보다 더 유연한 뮤텍스 관리가 가능합니다. 예: 지연 잠금, 조건 변수와의 조합 등

void print_message(const std::string& msg) {
    std::unique_lock<std::mutex> lock(mtx);
    std::cout << msg << std::endl;
    // lock.unlock(); // 수동 해제도 가능
}

4. std::recursive_mutex

같은 쓰레드에서 여러 번 lock이 필요한 경우 사용합니다.

std::recursive_mutex rmtx;

void recursive_function(int count) {
    if (count <= 0) return;
    rmtx.lock();
    std::cout << "Depth: " << count << std::endl;
    recursive_function(count - 1);
    rmtx.unlock();
}

5. std::timed_mutex, std::recursive_timed_mutex

특정 시간 내에 lock을 얻지 못하면 포기하도록 설계된 뮤텍스입니다.

std::timed_mutex tmtx;

void try_lock_example() {
    if (tmtx.try_lock_for(std::chrono::milliseconds(100))) {
        std::cout << "Lock acquired" << std::endl;
        tmtx.unlock();
    } else {
        std::cout << "Timeout: failed to acquire lock" << std::endl;
    }
}

6. std::lock 함수

여러 뮤텍스를 동시에 안전하게 lock할 때 사용합니다. 데드락 방지를 위한 도구입니다.

데드락 케이스

// 쓰레드 A
m1.lock();
m2.lock(); // 잠금 대기 중

// 쓰레드 B
m2.lock();
m1.lock(); // 잠금 대기 중 → 데드락 발생!

쓰레드는 제 각각 돌기 때문에, 쓰레드A, B가 m1, m2를 lock을 한 다음 lock을 하게 되는 경우가 발생하게 되면 서로가 가진 뮤텍스를 기다리다가 영원히 대기 상태에 빠지게 됩니다.

#include <iostream>
#include <mutex>
#include <thread>

std::mutex m1, m2;

void threadA() {
    std::lock(m1, m2); // 안전하게 두 뮤텍스를 모두 잠금
    std::lock_guard<std::mutex> lk1(m1, std::adopt_lock);
    std::lock_guard<std::mutex> lk2(m2, std::adopt_lock);
    std::cout << "Thread A acquired both locks\n";
}

void threadB() {
    std::lock(m2, m1); // 순서를 바꿔도 안전하게 잠금
    std::lock_guard<std::mutex> lk1(m2, std::adopt_lock);
    std::lock_guard<std::mutex> lk2(m1, std::adopt_lock);
    std::cout << "Thread B acquired both locks\n";
}

int main() {
    std::thread t1(threadA);
    std::thread t2(threadB);

    t1.join();
    t2.join();
}

여기서 std::lock은 내부적으로 양쪽 쓰레드에서 같은 방식으로 동기화를 해주기 때문에 데드락 없이 둘 다 잠금에 성공할 수 있어요.


7. 주의사항 요약

  • 뮤텍스는 가능한 한 짧은 영역에서만 잠그고 풀어야 함
  • lock을 해제하지 않고 예외가 발생하면 데드락이 발생할 수 있음 → RAII 스타일 사용 권장
  • 여러 뮤텍스를 동시에 잠글 경우 반드시 std::lock 사용
  • 가능한 경우 std::scoped_lock (C++17부터 도입)을 사용하면 더 안전하게 여러 뮤텍스를 다룰 수 있음
std::mutex m1, m2;

void example() {
    std::scoped_lock lock(m1, m2);  // C++17: 데드락 방지 + RAII
    // 임계 구역
}
 

 

'Develop > C&CPP' 카테고리의 다른 글

[cpp] thread  (0) 2025.04.20
[cpp] atomic  (0) 2025.04.20
[cpp] cpp17에서 달라진 점  (0) 2025.04.20
[cpp] cpp14에서 추가된 것  (0) 2025.04.20
[service] 윈도우 서비스 프로그램  (0) 2025.01.05

+ Recent posts