C++ 멀티스레딩: thread

프로세스와 스레드

프로세스: 시스템 리소스 할당의 최소 단위, 일반적으로 실행 중인 프로그램 인스턴스로 정의됨 스레드: 시스템 작업 스케줄링의 최소 단위

프로세스 간 통신: 파이프, 세마포어, 신호, 메시지 큐, 공유 메모리, 소켓 스레드 간 통신: 락 메커니즘, 세마포어 메커니즘, 신호 메커니즘, 배리어

동기화: 작업 조각의 순서를 보장하는 것 상호 배제: 동일한 시점에 하나의 스레드만 자원을 사용하도록 하여 데이터 일관성을 유지하는 것

동기화 방법

  • 원자적 연산
  • 세마포어(semaphore)
  • 읽기/쓰기 세마포어(rw_semaphore)
  • 상호 배제 락(mutex lock)
  • 스핀 락(spinlock)
  • 읽기/쓰기 락(rwlock)
  • 순차 락(seqlock)

순차적 자원 할당 방법 은행가 알고리즘, 데드락 생산자/소비자 문제 읽기/쓰기 문제 철학자의 저녁식사 문제, 상호 배제

C++ 멀티스레딩 프로그래밍에서 사용되는 헤더 파일은 다음과 같습니다.

<thread>
<mutex>
<condition_variable>

<future>
<promise>
<packaged_task>

<atomic>

<async>

데드락

데드락 발생의 네 가지 필요 조건

조건 설명
상호 배제 리소스는 동시에 한 프로세스만 사용 가능
요청 및 유지 프로세스가 리소스를 요청할 때 이미 점유하고 있는 리소스를 해제하지 않음
비강탈 프로세스가 얻은 리소스는 사용 완료 전까지 강제로 해제되지 않음
순환 대기 프로세스들 사이에 순환적인 리소스 대기 관계 형성

데드락 예방

데드락 발생 조건들을 깨트리는 방법

  • 상호 배제 조건 파괴
  • 비강탈 조건 파괴
  • 요청 및 유지 조건 파괴
  • 순환 대기 조건 파괴

데드락 회피

리소스 할당의 안전성을 검사하여 순환 대기를 방지하는 방법, 예: 은행가 알고리즘

데드락 감지

데드락 발생을 허용하되 감지 방법을 제공

데드락 해소

발생한 데드락을 해결하기 위해 리소스 강제 해제 또는 스레드 취소

C++11 스레드 라이브러리

C++11에서는 표준 스레드 라이브러리 <thread>를 도입하였으며, std 네임스페이스에 위치하며 컴파일 시 -std=c++11 옵션을 추가합니다.

생성자

  • 기본 생성자
thread() noexcept;

joinable하지 않은 빈 thread 객체를 생성합니다.

  • 초기화 생성자
template<typename Fn, typename... Args>
explicit thread(Fn&& fn, Args&&... args);

joinable한 thread 객체를 생성합니다. 새로운 스레드는 fn()을 호출하며, 인수는 args로부터 전달됩니다.

복사 생성자

thread(const thread&) = delete;

복사는 금지되어 있으며, thread 객체는 복사할 수 없습니다.

이동 생성자

thread(thread&& x) noexcept;

임시(익명) thread 객체를 다른 thread 객체로 이동할 수 있게 합니다. 성공 후, x는 더 이상 어떤 thread 객체도 나타내지 않습니다.

주의사항: 각 thread 객체는 소멸되기 전에 반드시 join()을 호출하여 주 스레드가 자식 스레드가 완료될 때까지 기다리거나 detach()를 호출하여 두 스레드를 분리해야 합니다. 그렇지 않으면 다음 문제가 발생할 수 있습니다:

  • 스레드가 차지한 리소스가 모두 해제되지 않아 메모리 누수가 발생합니다.
  • 주 스레드가 완료되었으나 자식 스레드가 아직 완료되지 않았을 경우 프로그램 실행이 예외를 발생시킵니다.

예제 코드는 다음과 같습니다:

#include <iostream>
#include <utility>
#include <thread>
#include <chrono>
#include <functional>
#include <atomic>

void 함수1(int 번호) {
    for (int i = 0; i < 5; ++i) {
        std::cout << "스레드 " << 번호 << " 실행 중\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

void 함수2(int& 카운터) {
    for (int i = 0; i < 5; ++i) {
        std::cout << "스레드 2 실행 중\n";
        ++카운터;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

int main() {
    int 카운터 = 0;
    std::thread t1; // t1은 스레드가 아닙니다.
    std::thread t2(함수1, 카운터 + 1); // 값으로 전달
    std::thread t3(함수2, std::ref(카운터)); // 참조로 전달
    std::thread t4(std::move(t3)); // t4는 이제 함수2()를 실행 중입니다. t3은 더 이상 스레드가 아닙니다.
    t2.join();
    t4.join();
    std::cout << "카운터의 마지막 값은 " << 카운터 << '\n';
}

대입 연산자

  • 복사 대입
thread& operator=(const thread&) = delete;

복사는 금지되어 있으며, thread 객체는 복사할 수 없습니다.

  • 이동 대입
thread& operator=(thread&& rhs) noexcept;

대입 대상 객체가 joinable하지 않은 경우 오른쪽 값을 가진 임시 객체 rhs를 전달해야 합니다. 그렇지 않으면 terminate() 에러가 발생합니다.

예제 코드는 다음과 같습니다:

#include <iostream>
#include <chrono>
#include <thread>

void 스레드_작업(int 초) {
    std::this_thread::sleep_for(std::chrono::seconds(초));
    std::cout << "안녕하세요, 스레드 "
              << std::this_thread::get_id()
              << ", " << 초 << "초간 대기했습니다.\n";
}

int main() {
    std::thread 스레드들[5];
    std::cout << "5개의 스레드 생성...\n";
    for (int i = 0; i < 5; i++) {
        스레드들[i] = std::thread(스레드_작업, i + 1);
    }
    std::cout << "스레드 생성 완료! 이제 합류를 기다립니다\n";
    for (auto& t : 스레드들) {
        t.join();
    }
    std::cout << "모든 스레드 합류 완료.\n";

    return 0;
}

다른 멤버 함수들

함수 설명
joinable() 스레드 객체가 join 함수를 호출할 수 있는지 여부를 반환
join() 현재 스레드를 블록하여 스레드 객체가 실행 완료될 때까지 기다림
detach() 스레드 객체를 해당 스레드에서 분리하여 독립적으로 실행되도록 함
swap() 두 스레드 객체가 표현하는 기본 핸들을 교환
native_handle() std::thread 구현에 따라 특정 스레드 핸들을 반환
hardware_concurrency()[static] 하드웨어 동시성 특성을 탐지하여 지원하는 스레드 동시성 수를 반환, 단 반환값은 시스템 힌트(hint)로서 사용

주의사항

  1. 만약 스레드가 작업을 완료했지만 join되지 않았다면, 해당 스레드는 여전히 활성화된 것으로 간주되며, 따라서 join할 수 있습니다.
  2. detach()를 호출한 후에는
  • *this는 더 이상 스레드 인스턴스를 나타내지 않습니다.
  • joinable() == false
  • std::this_thread::get_id() == std::thread::id()

std::this_thread 네임스페이스 내의 관련 유틸리티 함수

<thread> 헤더 파일은 thread 클래스뿐만 아니라 this_thread라는 이름의 네임스페이스도 정의하며, 여기에는 유용한 함수들이 포함되어 있습니다.

함수 설명
get_id() 현재 스레드 객체 ID 반환
yield() 현재 스레드를 블록하여 운영 체제가 다른 스레드를 실행하도록 함
sleep_until() 지정된 시간까지 현재 스레드를 블록
sleep_for() 지정된 시간 동안 현재 스레드를 대기, 스레드 스케줄링 등의 이유로 실제 대기 시간은 sleep_duration보다 더 길 수 있음

예제 코드는 다음과 같습니다:

#include <iostream>
#include <utility>
#include <thread>
#include <chrono>
#include <functional>
#include <atomic>

void 함수1(int 번호) {
    for (int i = 0; i < 5; ++i) {
        std::cout << "스레드 " << 번호 << " 실행 중\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

void 함수2(int& 카운터) {
    for (int i = 0; i < 5; ++i) {
        std::cout << "스레드 2 실행 중\n";
        ++카운터;
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
}

int main() {
    int 카운터 = 0;
    std::thread t1; // t1은 스레드가 아닙니다.
    std::thread t2(함수1, 카운터 + 1); // 값으로 전달
    std::thread t3(함수2, std::ref(카운터)); // 참조로 전달
    std::thread t4(std::move(t3)); // t4는 이제 함수2()를 실행 중입니다. t3은 더 이상 스레드가 아닙니다.
    t2.join();
    t4.join();
    std::cout << "카운터의 마지막 값은 " << 카운터 << '\n';
}

태그: C++ Multithreading Thread synchronization deadlock

6월 16일 20:34에 게시됨