본문 바로가기
프로그래밍/C++

C++ ] std::thread 스레드 사용법

by eteo 2023. 9. 25.

 

 

스레드 라이브러리

 

스레드를 사용하는 주요 목적은 병렬 프로그래밍을 통해 여러 작업을 동시에 실행시켜 프로그램의 효율성을 높이는 것이다.

예전에는 멀티스레드 프로그래밍을 하기 위해서든 윈도우의 경우 Win32 API를 사용하거나, 리눅스는 POSIX thread (pthread) API를 사용하었는데 C++ 11부터는 C++ 표준 라이브러리에서 스레드를 지원되기 때문에 <thread> 헤더를 포함하고 std::thread 클래스를 사용하면 된다.

 

 

 

1. 기본 사용법

 

std::thread 생성자는 스레드가 시작될 때 실행할 함수를 인자로 받는다. 이 때 함수는 글로벌 함수, 멤버 함수, 람다 표현식 등 다양한 형태일 수 있는데 하나씩 살펴보자.

 

 

1.1 글로벌 함수 전달하기

#include <iostream>
#include <thread>

using namespace std;

void threadFunction() {
    for (int i = 0; i < 5; ++i) {
        cout << "스레드에서 작업 중: " << i << endl;
    }
}

int main() {
    // 스레드 생성 및 시작
    thread t(threadFunction);

    // 메인 스레드에서 수행할 작업
    for (int i = 0; i < 3; ++i) {
        cout << "메인 스레드에서 작업 중: " << i << endl;
    }

    // 스레드 종료 대기
    t.join();

    return 0;
}

 

 

1.2 람다식 전달하기

#include <iostream>
#include <thread>

using namespace std;

int main() {
    // 람다식을 사용하여 스레드 생성 및 실행
    thread t([]() {
        for (int i = 0; i < 5; ++i) {
            cout << "스레드 t에서 작업 중: " << i << endl;
        }
    });

    // 메인 스레드에서 수행할 작업
    for (int i = 0; i < 3; ++i) {
        cout << "메인 스레드에서 작업 중: " << i << endl;
    }

    // 스레드 종료 대기
    t.join();

    return 0;
}

 

 

1.3 멤버함수 전달하기

#include <iostream>
#include <thread>

using namespace std;

class MyClass {
public:
    void memberFunction(int x) {
        for (int i = 0; i < x; ++i) {
            cout << "작업 중: " << i << endl;
        }
    }
};

int main() {
    MyClass myObject;
    // 첫번째 인자로 멤버 함수를 전달할 때는 두번째 인자로 해당 객체 인스턴스를 같이 전달한다.
    thread t(&MyClass::memberFunction, &myObject, 5);

    t.join(); // 스레드 종료 대기

    return 0;
}

 

 

 

2. 스레드에 인수 전달하기

 

std::thread 생성자에 해당 인수를 전달하면 되며, 여러개를 전달할 수도 있다.

 

#include <iostream>
#include <thread>

using namespace std;

void threadFunction(const string &message, int num) {
    cout << message << " " << num << endl;
}

int main() {
    string arg1 = "Hello world!";
    int arg2 = 10;
    thread t(threadFunction, arg1, arg2);
    t.join();

    return 0;
}

 

 

 

 

3. 스레드 객체를 먼저 선언하고 나중에 스레드 함수로 초기화하는 방법

 

#include <iostream>
#include <thread>

using namespace std;

void threadFunction(const string &message) {
    cout << message << " " << endl;
}

int main() {
    
    thread t;
    t = thread(threadFunction, "Hello world!");
    
    t.join();

    return 0;
}

 

 

 

 

4. join()

 

join()함수는 스레드의 종료를 기다리는 함수로, join()함수가 호출되면 스레드가 실행을 마칠 때까지 리턴되지 않으며 이를 호출한 스레드는 블록된 상태로 해당 스레드의 작업이 완료될 때까지 대기하게 된다.

 

이렇게 스레드의 종료를 기다려야 하는 이유는 thread 객체가 스코프를 벗어나거나 소멸될 때 해당 스레드 함수가 여전히 실행중인 경우 std::terminate() 함수가 호출되고 프로그램이 비정상적으로 종료되기 때문이다. (terminate called without an active exception)

 

때문에 thread의 생명주기 내에서 리소스가 정상적으로 정리될 수 있도록 join()함수를 통해 기다릴 필요가 있다.

 

 

#include <iostream>
#include <thread>

using namespace std;

void threadFunction() {
    this_thread::sleep_for(chrono::milliseconds(1000));
}

int main() {
    
    thread t(threadFunction);
//    t.join();

    return 0;
}

 

 

 

 

 

 

5. joinable()

 

joinable() 함수는 스레드가 join() 가능한지 여부를 판단한다. 스레드는 생성되면 고유한 ID를 가지게 되는데 이 스레드 ID는 join() 또는 detach() 함수를 통해서만 0이 되게 된다.

 

즉, joinable() 함수는 현재 스레드가 활성 상태인지 체크하는데 용도이며, joinable() 함수의 반환값이 true인 경우 스레드가 아직 join()을 통해 종료되지도 않았고 detach()를 통해 분리되지도 않았으니 join()을 호출할 수 있음을 의미한다. 이를 통해 join() 함수가 두번 이상 호출되는 오류를 방지할 수 있다.

 

그리고 detach()로 분리된 스레드는 부모 스레드에서 joinable() 함수를 호출해도 항상 false를 반환한다. detach() 를 호출하면 해당 스레드와 관련된 리소스와 제어권이 분리되어 부모 스레드는 해당 스레드의 상태를 제어할 권리를 다시 얻을 수 없기 때문이다.

 

 

if (t.joinable()) {
    t.join();
}

 

 

 

 

 

 

6. detach()

 

detach() 함수는 스레드를 분리한다. 부모 스레드가 해당 스레드의 종료를 기다리지 않고 다른 작업을 계속할 수 있으며, 자식 스레드가 끝나기 전에 부모 스레드가 먼저 끝날 수 있다. 분리된 스레드는 백그라운드에서 실행되며, 스레드가 종료되면 자동으로 정리된다.

다만 detach()를 호출하면 스레드 ID가 0이 되고 해당 스레드의 상태를 제어할 수 없다.

 

t.detach();

 

 

 

 

 

7. 분리된 스레드에 종료신호 보내서 종료하기

 

멀티스레드 환경에서 여러 스레드가 하나의 변수에 접근 할 때변수의 원자적 조작을 보장하는 atomic 객체를 플래그로 사용하여 스레드에서 모든 작업을 마치고 안전하게 종료하게끔 할 수 있다.

 

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

using namespace std;

atomic<bool> stop_flag(false);
atomic<bool> end_flag(false);

void workerThread() {
    while (1) {
        cout << "워커 스레드 실행중" << endl;
        if (stop_flag.load())
        {
            break;
        }
    }
    end_flag.store(true);
}

int main() {
    thread t(workerThread);
    t.detach();
    this_thread::sleep_for(chrono::seconds(1));
    stop_flag.store(true);

    while (!end_flag.load());

    cout << "정상 종료" << endl;

    return 0;
}

 

 

 

 

 

 

8. std::this_thread

 

this_thread는 C++ 표준 라이브러리에서 제공하는 네임스페이스이며, 현재 실행중인 스레드에 대한 정보와 동작을 제어하기 위한 여러 함수와 타입들을 포함하고 있다.

 

this_thread 네임스페이스를 사용하면 thread 객체 없이현재 스레드에 접근할 수 있다.

 

this_thread 네임스페이스 제공하는 함수들

 

1. std::this_thread::sleep_for : 현재 스레드를 일정 시간 동안 대기시키는 함수

2. std::this_thread::sleep_until : 현재 스레드를 특정 시점까지 대기시키는 함수

3. std::this_thread::get_id : 현재 스레드의 ID를 얻는 함수

4. std::this_thread::yield : 자신의 차례를 다른 스레드에게 양보하고 현재 스레드를 스케줄링 대기 상태로 전환

 

#include <iostream>
#include <thread>

using namespace std;

int main() {
    cout << "Start" << endl;

    this_thread::sleep_for(chrono::milliseconds (1000));

    cout << "End" << endl;

    return 0;
}

 

 

 

 

 

9. 스레드 ID 확인하기

 

std::thread::get_id() 함수는 스레드의 고유한 식별자인 std::thread::id 타입을 반환한다. 이 타입은 멀티스레드 환경에서 == 또는 != 연산자를 사용하여 스레드 ID를 비교하는 데 활용될 수 있다.

 

#include <iostream>
#include <thread>

using namespace std;

void threadFunction() {
    this_thread::sleep_for(chrono::seconds(2));
}

int main() {
    
    thread::id id_this = this_thread::get_id();
    cout << "현재 스레드의 ID: " << id_this << endl;
    
    thread t(threadFunction);
    thread::id id_child = t.get_id();
    cout << "자식 스레드의 ID: " << id_child << endl;
    
    t.join();
    
    return 0;
}

 

 

 

 

 

 

 

 

기본 개념

 

프로세스란?

CPU의 시간을 할당받아 실행 중인 프로그램을 말한다. 프로그램이 저장 장치에 실행파일로 존재하는 정적인 개념이라면, 프로세스는 코드(CPU 명령), 데이터(전역변수, 정적변수), 리소스(그림파일, 사운드 파일 등)를 실행파일에서 읽어들여 작업을 수행하는 동적인 개념이다. 각 프로세스는 독립적인 실행단위로 다른 프로세스의 메모리 공간에 접근하려면 IPC 매커니즘을 사용해야 한다.

 

스레드란?

스레드는 프로세스의 하위 개념으로, CPU시간을 할당 받아 프로세스 메모리 영역에 있는 데이터를 사용하고 코드를 수행하는 실행 흐름이다. 모든 프로세스는 한 개 이상의 스레드를 가지고 있다. 응용 프로그램 실행 시 최초로 생성되는 스레드를 메인 스레드라고 하며 보통 main() 함수에서 시작한다. 같은 프로세스 내 스레드 간에는 데이터와 자원을 공유할 수 있다.

 

멀티스레드란?

메인 스레드와 별도로 동시에 수행하고자 하는 작업이 있다면 스레드를 추가로 생성하여 추가한 스레드가 해당 작업을 수행하도록 하면된다. 스레드를 추가하면 이후엔 운영체제가 알아서 컨텍스트 스위칭을 하며 스레드들을 병렬적으로 동시에 처리한다.

 

컨텍스트 스위칭이란?

스레드를 짧은 간격으로 교대로 실행하면 동시에 실행되는 것처럼 느껴지는데, 이때 스레드의 최종 실행 상태(CPU 레지스터, 메모리의 스택)를 저장하고, 복원하는 작업을 반복해야 한다. 이를 컨텍스트 전환이라고 한다.

 

 

기본 개념 Reference : TCP/IP 소켓 프로그래밍, 김선우 저