Multi thread 프로그래밍을 할때 현재 thread가 작업하는 메모리를 다른 thread에 덮어쓰거나 잘못 사용하는 일이 발생하지 않도록 주의해야한다.

이러한 문제를 대비하기 위한 기법이 바로 thread 동기화인데 Critical Section, Semaphore, Mutex 등이 있다.

이러한 것들을 사용하지 않는다면

 

Tread 동기화를 사용하지 않은 스레드 작업

#include <iostream>
#include <Windows.h>
#include <thread>
#include <memory>
#include <mutex>
using namespace std;
int iArr[500];      // 두 thread가 작업할 메모리 공간

void test1()    // test1 함수는 iArr에 0을 채운다.
{
    for (int i = 0; i < 500; i++)
    {
        iArr[i] = 0;
        Sleep(1);
    }
}

void test2()    // test2 함수는 iArr에 1을 채운다.
{
    for (int i = 0; i < 500; i++)
    {
        iArr[i] = 1;
        Sleep(1);
    }
}

int main()
{
    // 각 함수를 스레드로 작동
    thread t1(test1);
    thread t2(test2);

    // 두 함수가 끝나기를 기다린다.
    t1.join();
    t2.join();

    // 결과값 출력
    for (int i = 0; i < 500; i++)
    {
        cout << iArr[i];
        if ((i +1) % 50 == 0)
        {
            cout << endl;
        }
    }
}

 

 

위의 결과 처럼 중간 중간 값을 덮어씌워서 오작동을 일으킬 수 있다.

 

하지만 위에서 말한 thread 동기화 기법을 사용하면 이 문제는 해결된다.

 

 

Mutex를 사용한 thread 작업

#include <iostream>
#include <Windows.h>
#include <thread>
#include <memory>
#include <mutex>
using namespace std;

int iArr[500];
mutex m;    // Thread Lock을 걸 Mutex 클래스

void test1()
{
    m.lock();   // 메모리에 lock을 걸어 다른 thread에서 사용하지 못하게 한다.
    for (int i = 0; i < 500; i++)
    {
        iArr[i] = 0;
        Sleep(1);
    }
    m.unlock(); // 메모리에 대한 작업이 끝난 이후에 lock을 해제한다.
}

void test2()
{
    m.lock();
    for (int i = 0; i < 500; i++)
    {
        iArr[i] = 1;
        Sleep(1);
    }
    m.unlock();
}

int main()
{
    // 각 함수를 스레드로 작동
    thread t1(test1);
    thread t2(test2);

    // 두 함수가 끝나기를 기다린다.
    t1.join();
    t2.join();

    // 결과값 출력
    for (int i = 0; i < 500; i++)
    {
        cout << iArr[i];
        if ((i +1) % 50 == 0)
        {
            cout << endl;
        }
    }
}

 

 

Mutex를 사용하면 처음 lock을 건 thread에서 작업이 끝난 이후에야 다른 thread에서 그 메모리에 접근해서 작업이 가능하기 때문에 모든 배열에 1이 출력이 된다.

 

 

 

이렇게 별 문제가 없어 보이는 thread lock에는 문제가 있는데 바로 Race Condition이다. 이 race condition은 일반적으로 1번 thread와 2번 thread가 있고 데이터가 담긴 메모리 A, B가 있을 때,  1번 thread가 A메모리에 lock을 건 상태에서 B메모리에 작업을 하려고 하고, 2번 thread는 B메모리에 lock을 건 상태에서 A메모리에 작업을 하려고 할때 발생한다. 이렇게 되면 1번 thread와 2번 thread는 서로 자신의 작업이 끝나야 각각의 메모리의 lock 해제하는데 서로에게 lock이 걸린 메모리 때문에 작업을 그 이후로 진행할 수 없기 때문에 프로그램은 작동을 정지하고 만다. 이런 상황을 다른 말로 dead lock, 교착상태라고도 한다.

 

하지만 scoped lock은 사실 이런 완벽한 교착 상태를 예방하기 위한 것이라기 보다는 사소한 프로그래머의 실수를 방지하기 위한 것이다. 다음의 코드를 보자.

 

프로그래머의 실수!

#include <iostream>
#include <Windows.h>
#include <thread>
#include <memory>
#include <mutex>
using namespace std;
int iArr[500];
mutex m;
void test1()
{

    // 이 프로그래머는 훌륭하게 lock을 걸었지만
    m.lock();
    for (int i = 0; i < 500; i++)
    {
        iArr[i] = 0;
        Sleep(1);
    }
    // unlock을 까먹고 하지 않았다!
}

void test2()
{
    m.lock();
    for (int i = 0; i < 500; i++)
    {
        iArr[i] = 1;
        Sleep(1);
    }
    m.unlock();
}

int main()
{
    thread t1(test1);
    thread t2(test2);
    t1.join();
    t2.join();
    for (int i = 0; i < 500; i++)
    {
        cout << iArr[i];
        if ((i +1) % 50 == 0)
        {
            cout << endl;
        }
    }
}

 

위의 코드처럼 실수로 thread lock을 건 이후에 작업이 끝나고 unlock을 까먹는다면 2번 thread는 1번 thread가 걸어둔 lock이 해제되기를 영원히 기다릴 것이다. 위 코드처럼 간단한 thread 예제라면 이러한 문제를 쉽게 찾아내겠지만 수만줄을 넘어가고 여러 종류의 thread가 여러 개 돌아가는 커다란 프로그램에서 저런 기억하기 어려운 사소한 실수를 범하게 된다면 얼마나 긴 시간을 허비하게 될까?

 

 

 

그래서 나온 것이 scoped lock이라는 것인데 이것의 원리는 매우 간단하다. 우리가 사용하는 Mutex를 하나의 클래스로 가볍게 감싸는 것이다. 그리고 클래스의 생성자가 실행될 때 lock()을 실행하고 클래스의 소멸자가 실행될 때 unlock()을 실행해 주는 것이다.

이것은 프로그래머가 직접 구현해도 될 정도로 간단한 작업이지만 표준 C++에서는 이미 지원되고 있으니 해당 기능을 사용하면 된다.

 

Scoped Lock 사용법

#include <iostream>
#include <Windows.h>
#include <thread>
#include <memory> // Scoped lock 기능을 사용하기 위해 include 해야하는 header
#include <mutex>
using namespace std;
int iArr[500];
mutex m;
void test1()
{
    lock_guard<mutex> g(m);      
    // lock_guard 클래스의 템플릿에 mutex 클래스를 넣고 생성자에 우리가 사용하는 mutex 객체를 넣어주면 된다.
    // lock_guard 객체가 생성될 때 자동으로 thread에 lock이 걸린다.
    for (int i = 0; i < 500; i++)
    {
        iArr[i] = 0;
        Sleep(1);
    }
    // 그리고 함수가 끝날 때 or 스코프를 벗어날 때 자동으로 lock_guard 객체가 소멸되면서 thead lock이 해제된다.
}
void test2()
{
    lock_guard<mutex> g(m);
    for (int i = 0; i < 500; i++)
    {
        iArr[i] = 1;
        Sleep(1);
    }
}
int main()
{
    thread t1(test1);
    thread t2(test2);
    t1.join();
    t2.join();
    for (int i = 0; i < 500; i++)
    {
        cout << iArr[i];
        if ((i +1) % 50 == 0)
        {
            cout << endl;
        }
    }
}
반응형

'C++' 카테고리의 다른 글

[C++11] enum class  (1) 2017.07.17
[C++ 11] static_assert  (0) 2017.05.23
[C++ 11] Auto Vectorization  (1) 2016.11.01
[C++ 11] Range-Based For  (0) 2016.11.01
[C++ 11] Scoped Lock  (0) 2016.11.01

+ Recent posts