본문으로 바로가기

C++11) std::thread 와 관련된 함수, 라이브러리 1

category C++/Modern 2019. 2. 7. 03:22

<개요>

 

사실 전시간의 explicit.... 를 끝으로 C++11 의 내용을 끝내려 했는데 아직 멀티스레드에 대한 글을 작성하지 않아서 하나 더 추가해 작성합니다.

이번 글에선 std::thread 그리고 그와 관련된 함수 ( std::mutex, atomic, etc... ) 에 대해 작성합니다.

 

<std::thread>
std::thread 는 라이브러리에 포함돼있습니다.
함수 객체, 함수 포인터, 람다식 과 id 를 제공하면 자동으로 thread 를 제작해주며 join 함수로 blocking 할 수 있습니다.

class func1 {
public:
    func1(int _id, int _length) : m_nid(_id), m_nlength(_length) {}
    void operator()() {
        for (int i = 0; i < m_nlength; ++i)
            cout << "id : " << m_nid << " value : " << i << endl;
    }
private:
    int m_nid;
    int m_nlength;
};
void func2(int id, int length) {
    for (int i = 0; i < length; ++i)
        cout << "id : " << id << " value : " << i << endl;
}

int main() {
    thread t1(func2, 1, 5);
    thread t2(func2, 2, 5);    // 함수를 넘기는 방법

    thread t3{ func1(3, 5)};
    func1 f1(4, 5);
    thread t4(f1);
    thread t5(func1(5, 5)); // 함수객체를 넘기는 방법

    thread t6([](int id, int length) {
        for( int i = 0 ; i < length ; ++i)
            cout << "id : " << id << " value : " << i << endl;
    }, 6, 5);    // 람다를 넘기는 방법
    
    thread t7;
    t7 = std::thread(func1(7, 5));    // 이런 식의 지연 시킨 뒤 사용도 가능

    t1.join();
    t2.join();
    t3.join();
    t4.join();
    t5.join();
    t6.join();
    t7.join();
}

join 함수는 스코프가 끝나기 전까지 스레드를 기다리는 함수입니다.

만약 스코프가 끝났는데도 스레드가 남아있을 경우, 에러가 발생합니다.

thread t5(func1(5,5)) 같은 경우는 thread t3{func1(5,5)} 에 비해 좋지 않은 방식입니다.

() 초기화 시 파라미터가 없다면 오류가 날 수 있기 때문입니다.

위 스레드의 출력내용은..

이런 식으로 뒤죽박죽 입니다.

 

<스레드 식별자>

thread 객체 내의 get_id() 함수를 통해 각각의 스레드의 id를 알아낼 수 있습니다.
이 get_id() 를 통해 특정 스레드만 리소스에 접근하게 하는 방식이 가능하고, 각각의 스레드를 구별할 수 있습니다.

void func2(int id, int length) {
    std::thread::id this_id = std::this_thread::get_id();    // 현재 함수에 진입한 스레드의 id를 알려줍니다.
    cout << this_id << endl;
    for (int i = 0; i < length; ++i)
        cout << "id : " << id << " value : " << i << endl;
}

int main() {
    thread t1(func2,5,5);
    std::thread::id th_id1 = t1.get_id(); //t1의 id를 저장합니다.

    cout << th_id1 << endl;
    cout << t1.get_id() << endl; // 이런식으로 get_id로 바로 참조할 수도 있습니다.

    t1.join();
}

std::thread::get_id 는 thread 객체의 id를 리턴합니다.

std::this_thread::get_id() 는 현재 스코프 내에서 작업중인 thread 의 id를 리턴합니다.

 

<std::thread::detach>

thread 객체와 실행 스레드를 분리시켜 독립적으로 실행되게 합니다.

detach를 호출하면 thread 객체는 더 이상 그 스레드를 호출 할 수 없습니다.

스레드는 종료되면 자동으로 할당 해제 됩니다.

만약 joinable == false 라면 에러가 발생합니다

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void func2(int id, int length) {
    for (int i = 0; i < length; ++i)
        cout << "id : " << id << " value : " << i << endl;
}

int main() {
    thread t1(func2,5,5);
    t1.detach();
    std::this_thread::sleep_for(std::chrono::seconds(5));    // 지금 실행중인 스레드를 5초동안 쉬게하는 함수
}

<sleep_for, sleep_until, yield>

스레드를 일시중지 시키거나 (sleep_for, sleep_until) 양보(yield) 하는 함수입니다.

#include <iostream>
#include <thread>
#include <chrono>
using namespace std;

void func2(int id, int length) {
    for (int i = 0; i < length; ++i)
        cout << "id : " << id << " value : " << i << endl;
}

int main() {
    thread t1(func2,5,5);
    std::this_thread::sleep_for(std::chrono::seconds(5));    // 지금 실행중인 스레드를 5초'동안' 쉬게하는 함수    
    std::this_thread::sleep_until((std::chrono::system_clock::now() + std::chrono::seconds(10)));    
    // 현재 시간이 만약 12:00:00 이라면, 12:00:10 '까지' 쉽니다

    t1.join();
}

for와 until의 차이를 아시겠나요? for는 ~'동안' 휴식이고 until은 지정된 시간 '까지' 휴식입니다.

yield는 자신을 제외한 다른 우선순위가 높은 스레드에게 작업을 양보합니다.

void func2(int id, int length) {
    std::this_thread::yield();    // 다른 우선순위가 높은 스레드에게 작업을 넘깁니다.
    for (int i = 0; i < length; ++i)
        cout << "id : " << id << " value : " << i << endl;
}

int main() {
    thread t1(func2,5,5);
    t1.join();
}

정보 ) yield 와 sleep_for 의 차이점

링크 : https://code.i-harness.com/ko-kr/q/1085f40

 

<race condition 과 mutex>

race condition이란, 한 리소스를 가지고 여러 thread가 접근하면서 생기는 문제라고 할 수 있습니다.

#include <iostream>
#include <thread>
using namespace std;

int i = 0;
void func1() {
    for (int j = 1; j <= 1000000 ; ++j)
        i++;
}
int main() {
    thread t1(func1);
    thread t2(func1);

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

    cout << i << endl;    //과연 20000000 일까요?
}

안타깝게도 i는 2000000에 미치지 않는 값입니다.

스레드가 들어가게 되면서 i의 값을 증가시키는데

만약 t1이 i가 1일때 증가를 시키게 되고 t2이 i가 1일때 증가를 시키게 되면

결국 t1과 t2를 합쳐서 i는 1밖에 증가를 못시키는 겁니다.

각 스레드가 데이터를 읽는 '시점'이 달라서 그렇게 되는 것인데, 

t1이 i를 1증가 시킨 상태에서 데이터를 업데이트 하지 않았는데,

t2가 i의 데이터를 가져가면 그 i는 t1이 아직 증가시키지 않은 값입니다. 그래서 동일한 값에 덮어쓰이게 되는 경우가 생깁니다.

이를 data race라고 합니다.

이를 해소하는 방법은 많이 있습니다. 그 중 간단한 mutex 객체를 통한 관리를 작성합니다.

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int i = 0;
std::mutex mt;
void func1() {
    mt.lock();
    for (int j = 1; j <= 1000000; ++j)
        i++;
    mt.unlock();
}
int main() {
    thread t1(func1);
    thread t2(func1);

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

    cout << i << endl;
}

mutex 는 임계구역 ( critical section )을 이용해 (Windows 에선) 자원을 사용할 때 lock을 걸어 자원을 독점합니다.

t1, t2가 func1에 접근하게 되면서 t1이 먼저 접근을 하게 되고, mt가 lock 상태가 아니였으므로 먼저 자원을 독점합니다.

t2는 t1이 lock으로 자원을 독점하고 있으니 unlock이 호출될때까지 대기합니다.

그리고 t1이 unlock을 호출하면 t2는 lock으로 자원을 독점합니다.

 

std::mutex엔 try_lock 이라는 함수가 있습니다.

이 함수는 lock을 시동하는데, 이미 lock이 걸려있다면 false를 리턴합니다.

#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

int i = 0;
std::mutex mt;
void func1() {
    if (mt.try_lock() == false)
        return;

    for (int j = 1; j <= 1000000; ++j)
        i++;
    mt.unlock();
}
int main() {
    thread t1(func1);
    thread t2(func1);

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

    cout << i << endl;    // t2 스레드가 바로 종료되므로 값은 1000000 입니다.
}