(C++ : IOCP-2) Thread 기초 정리

Posted by : at

Category : Cpp   iocp


std::thread 생성

윈도우에 종속적인 API를 사용할수 있지만 C++11에서 추가된 std::thread를 이용해서 thread를 생성하는 것을 추천한다.

#include <thread>

void HelloThread()
{
    cout <<" Hello Thread" << endl;
}

int main()
{
    // thread 생성
    std::thread t(HelloThread);

    cout << "Hello Main" << endl;

    // 이외에 여러함수가 지원됨.
    int32 count = t.hardwhare_concurrency();  // CPU코어 개수를 리턴
    auto id = t.get_id(); // thread id

    t.join();           // thread를 대기해준다.(이걸사용하지 않을경우 main이 먼저종료되어 Error처리됨)

    t.detach();         // main thread와 연결을 끊는다.(거의 사용안됨.)
    t.joinable();       // 연결(main thread에서 대기가능)이 가능한지 확인
}

여러 thread동시에 생성해보기

// 배개변수 받기, thread여러개 생성해보기
void HelloThread(int num)
{
    cout <<" Hello Thread" << num << endl;
}

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

    for(int32 i = 0; i < 10; i++)
    {
        v.push_back(std::thread(HelloThread, i));
    }

    for(int32 i = 0; i < 10; i++)
    {
        if(v[i].joinable())
            v[i].join();
    }
}

Atomic

// 힙 or 데이터 영역의 변수
int32 sum = 0;

void Add()
{
    for(int32 i = 0; i < 1'000'000; i++)
    {
        sum++;
    }
}

void Sub()
{
    for(int32 i = 0; i < 1'000'000; i++)
    {
        sum--;
    }
}

int main()
{
    std::thread t1(Add);
    std::thread t2(Sub);

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

    cout << sum << endl;
    // 0이 안나오게 된다. -> Thread사용의 주의사항
}
#include <atomic>

// 여러 Thread에서 접근하는 변수는 이렇게 선언
atomic<int32> sum = 0;

void Add()
{
    for(int32 i = 0; i < 1'000'000; i++)
    {
        sum.fetch_add(1);
    }
}

void Sub()
{
    for(int32 i = 0; i < 1'000'000; i++)
    {
        sum.fetch_add(-1);
    }
}

int main()
{
    std::thread t1(Add);
    std::thread t2(Sub);

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

    cout << sum << endl;
    // 0이 나온다
}

단, atomic은 연산이 느리기에 절대적으로 필요한 경우에만 사용하자


std::mutex

atomic보다 좀 더 효율적인 방법을 알려준다.

vector<int32> v;

void Push()
{
    for(int32 i = 0; i < 10'000; i++)
    {
        v.push_back(i);
    }
}

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

    // crash!! -> vector는 multi thread환경에 safe하지 않다
    // vector는 공간이 부족할경우 메모리를 새로할당 받는데 그때 crash발생
    t1.join();
    t2.join();

    cout << v.size() << endl;
}

해결법

#include <mutex>

vector<int32> v;
mutex m;

void Push()
{
    for(int32 i = 0; i < 10'000; i++)
    {
        m.lock();
        v.push_back(i);
        m.unlock();

        // 하지만 이 방식은 동작속도가 매우 느리다. -> Mutual Exclusive(상호배타적)
        // 그리고 Deadlock을 조심해야한다.
    }
}

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

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

    cout << v.size() << endl;
}

조금 더 안전한 코드를 만들어 보자.

#include <mutex>

vector<int32> v;
mutex m;

void Push()
{
    for(int32 i = 0; i < 10'000; i++)
    {
        std::lock_guard<std::mutex> lockGuard(m);
        // or
        std::unique_lock<std::mutex> uniqueLock(m);
        // std::unique_lock<std::mutex> uniqueLock(m, std::defer_lock);
        // std::defer_lock : 당장은 잠그지말고 .lock()을 호출하면 잠궈달라
        v.push_back(i);
    }
}

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

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

    cout << v.size() << endl;
}

Deadlock 예시

lockguard로 모든 데드락을 방지할순 없다.
아래의 예시를 살펴보자.

class User
{
    // ...
};

class UserManager
{
public:
    static UserManager* Instance()
    {
        static UserManager instance();
        return &instance;
    }

    User* GetUser(int32 id)
    {
        lock_guard<mutex> guard(_mutex);
        return nullptr;
    }

    void ProcessSave()
    {
        lock_guard<mutex> guard(_mutex);

        Account* account = AccountMnager::Instance()->GetAccount(100);

        // ...
    }

private:
    mutex _mutex;
};
class Account
{
    // ...
};

class AccountManager
{
public:
    static AccountManager* Instance()
    {
        static AccountManager instance();
        return &instance;
    }

    Account* GetAccount(int32 id)
    {
        lock_guard<mutex> guard(_mutex);
        return nullptr;
    }

    void ProcessLogin()
    {
        lock_guard<mutex> guard(_mutex);

        User* user = UserManager::Instance()->GetUser(100);

        // ...
    }

private:
    mutex _mutex;
};
void Func1()
{
    for(int32 i = 0; i < 100; i++)
    {
        UserManager::Instance()->ProcessSave();
    }
}

void Func2()
{
    for(int32 i = 0; i < 100; i++)
    {
        AccountManager::Instance()->ProcessLogin();
    }
}

int main()
{
    std::thread t1(Func1);
    std::thread t2(Func2);

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

어떤상황에 Deadlock이 발생할까

ProcessSave(), ProcessLogin()이 호출될 경우 lock이 발생하고 ProcessSave(), ProcessLogin()내부에서 GetAccount, GetUser 호출시 역시 lock이 발생하며 Deadlock이 된다.

해결책?? -> mutex lock을 잠그는 순서를 지정한다.

void ProcessLogin()
{
    User* user = UserManager::Instance()->GetUser(100);

    // 아래서 lock
    lock_guard<mutex> guard(_mutex);

    // ...
}

About Taehyung Kim

안녕하세요? 8년차 현업 C++ 개발자 김태형이라고 합니다. 😁 C/C++을 사랑하며 다양한 사람과의 협업을 즐깁니다. ☕ 꾸준한 자기개발을 미덕이라 생각하며 노력중이며, 제가 얻은 지식을 홈페이지에 정리 중입니다. 좀 더 상세한 제 이력서 혹은 Private 프로젝트 접근 권한을 원하신다면 메일주세요. 😎

Star
Useful Links