C++

멀티 쓰레드

춤추는수달 2022. 1. 24. 20:42

c++ 11 이후 에선 #include <thread>, std::thread(쓰레드함수) 로 Windows 환경에 종속적이지 않은 멀티 쓰레드를 구현할 수 있다.

  • 엔트리 포인트 : 쓰레드 함수
  • 자식 쓰레드 t가 종료되기 전에 메인 쓰레드가 먼저 종료되면 에러가 뜸. 
  • 디버그 시 각 쓰레드별 실행중 문장 확인 가능.

Visual Studio 디버그 중 스레드 별 실행 확인

 

쓰레드 주요 함수들
  • t.hardward_concurrency() : CPU 코어 갯수
  • t.get_id() : 쓰레드의 id
  • t.detach() : 쓰레드 객체 t와 실질적으로 동작하는 쓰레드의 연결을 끊어버림. 데몬 프로세스.
  • t.joinable() : t 객체에 실질적으로 연동된 쓰레드가 있는지 확인 = 쓰레드 id가 0인지 확인. (연동된 쓰레드가 없다면 쓰레드 id는 0이 됨)
  • t. join() : 자식 쓰레드가 종료될 때 까지 기다려줌.
  • t = thread(쓰레드 함수, 인자) : 쓰레드 객체 t 에 실제 연동된 쓰레드가 생김. 그리고 그 쓰레드는 '쓰레드 함수' 를 실행하고 '쓰레드 함수' 가 종료되면 같이 종료됨. 쓰레드 함수에 인자가 있다면 thread() 다음 인자로 넘겨주면 됨('인자' 매개변수).

 

공유 데이터

sum++, sum --  : 어셈블리로 3단계 -> 문제발생

 

atomic

#include <atomic> 운영체제에 독립적

atomic : All-Or-Noting

atomic<int32> sum = 0;

sum.fetch_add(1);sum.fetch_add(-1); (sum++, sum-- 도 오버로딩이 되어있어서 되긴 함.)

디스어셈블리로 봐도 조금 달라져있음.

atomic은 연산이 느리기떄문에 남발 금물.

 

Lock

stl은 기본적으로 멀티 쓰레드에서 동작 안됨.

vector가 다 차면 capacity를 늘린 새 공간에 복사한 다음 기존의 공간은 지워버림. 근데 이걸 두 쓰레드가 동시에 해버리면 꼬임. 그것 뿐 아니라 그냥 꼬임.

atomic은 vector같은 구조에 사용하기는 힘듦.

#include<mutex>로 통합됨. 

mutex m; 

m.lock(); // 화장실 문 잠구기. 한 번에 한 명만 들어갈 수 있음.

m.ulock();//화장실 문 열기

lock된 상태에서 다른 쓰레드가 같은 코드를 실행하면 lock을 실행 한 쓰레드가 unlock을 실행할 때 까지 멈춤.

즉 사실상 싱글 쓰레드와 같이 동작하게 됨. 상호 배타적인 특성을 가짐.

 mutex는 재귀적으로 lock을 걸 수 없음( lock 안에 lock).

 

RAII(Resource Acquisition Is Initialization) 패턴 :

wrapper 클래스를 만들어 생성자에서 자동으로 lock해주고 소멸자에서 unlock 해줌. unlock을 까먹는 등의 경우가 없으므로 코드 안정성이 올라감.

LockGuard가 있음.

LockGuard

의 구현은 대충 아래와 같은 형태임.

template<typename T>
class LockGuard
{
public :
	LockGuard(T& m)
	{
		_mutex = &m;
        _mutex->lock();
	}
    
    ~LockGuard()
    {
    	_mutex->unlock();
    }
	T* _mutex;
}

{
	mutex m;
	LockGuard<std::mutex> lockGuard(m); // LockGuard()생성자 호출 시 자동 lock.
    
} // ~LockGuard()소멸자 호출 시 자동 unlock

물론 표준 헤더에도 있는 기능이므로 직접 구현하진 말고 'std::lock_guard<std::mutex> lockGuard(m)' 와 같이 사용하자.

 

unique_lock :

lock_guard에 추가 기능이 있음. 생성 시 두 번째 인자에 넘겨주는 값에 따라 lock 시점을 조절할 수 있는 기능. 두 번쨰 인자로 어떤 값을 줄 수 있는지 알아보자.

  • std::defer_lock : 기본적으로 lock이 걸리지 않으며 잠금 구조만 생성됩니다. lock() 함수를 호출 될 때 잠금이 됩니다. 둘 이상의 뮤텍스를 사용하는 상황에서 데드락이 발생 할 수 있습니다.(std::lock을 사용한다면 해결 가능합니다.)
  • std::try_to_lock : 기본적으로 lock이 걸리지 않으며 잠금 구조만 생성됩니다. 내부적으로 try_lock()을 호출해 소유권을 가져오며 실패하더라도 바로 false를 반환 합니다. lock.owns_lock() 등의 코드로 자신이 락을 걸 수 있는 지 확인이 필요합니다.
  • std::adopt_lock : 기본적으로 lock이 걸리지 않으며 잠금 구조만 생성됩니다. 현재 호출 된 쓰레드가 뮤텍스의 소유권을 가지고 있다고 가정합니다. 즉, 사용하려는 mutex 객체가 이미 lock 되어 있는 상태여야 합니다.(이미 lock 된 후 unlock을 하지않더라도 unique_lock 으로 생성 해 unlock해줄수 있습니다.)

'std::unique_lock<std::mutex> uniqueLock(m, std::defer_lock);' 과 같이 사용함.

 

DeadLock

여러 Lock이 교착되면서 쓰레드가 무한히 unlock을 기다리는 상태를 말한다.

대표적인 상황은 아래와 같다.

mutex m1;
mutex m2;

void LockM1first()
{
	m1.lock();
    m2.lock();
    //do something
    m2.unlock();
    m1.unlock();
}
void LockM2first()
{
	m2.lock();
    m1.lock();
    //do something
    m1.unlock();
    m2.unlock();
}

void main()
{
	thread t1 = thread(LockM1first);
    thread t2 = thread(LockM2first);
}

위의 경우에서 t1이 m1.lock()을 통과하고, t2가 m2.lock() 을 통과한 상태가 되었을 때, t1은 m2.unlock()을 기다리고, t2는 m1.unlock()을 기다리게 될 것이다. 이렇게 서로가 서로를 기다리는 상황에서는 영원히 빠져나가지 못하고 DeadLock 상황이 벌어진다.

 

해결방법

두 함수 간에 lock 순서가 다르기 떄문에 이러한 문제가 일어난 것이므로 lock 순서를 맞추어주면 된다. 그런데 lock 순서를 맞추어 주는 방법은 프로그래머가 조심하면서 쓰는 방법 밖에 없다. 위 경우에는 아래처럼 바꿔주면 해결된다.

mutex m1;
mutex m2;

void LockM1first()
{
	m2.lock();
    m1.lock();
    //do something
    m1.unlock();
    m2.unlock();
}
void LockM2first()
{
	m2.lock();
    m1.lock();
    //do something
    m1.unlock();
    m2.unlock();
}

void main()
{
	thread t1 = thread(LockM1first);
    thread t2 = thread(LockM2first);
}

하지만 프로그램이 크고 복잡해지면 이렇게 lock 순서를 하나하나 맞추기 어려울 수 있다. 

사람들이 쓰는  몇 가지 lock순서 맞추기 기법을 알아보자.

  • lock에 id를 주는 방법이 있다. lock 마다 id를 부여하고 id 순서에 맞춰 lock을 해주는 것이다.
  • std::lock(m1, m2) 함수를 쓰는 방법도 있다. 이 함수를 쓰면 m1과 m2를 알아서 일관적인 순서로 lock 해준다. 참고로 2개 보다 더 많은 mutex 객체를 넣어도 된다. 후에는 lock_guard<mutex> g1(m1, std::adopt_lock); lock_guard g1(m1, std::adopt_lock); 문장을 써서 소멸할 때 자동 unlock 되도록 한다. 인자로 넘겨준 std::adopt_lock은 생성 시 lock은 하지 말고 소멸할 때 unlock만 해주라는 뜻이다.
  • lock들이 사이클 구조를 만드는지 검사해주는 방법도 있다. lock을 하는 순서로 그래프를 그릴 수 있는데, 이 그래프가 사이클을 만들면 DeadLock이 생길 수 있는 상태인 것이다.

 

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

메모리 모델  (0) 2022.02.01
Futrue  (0) 2022.01.27
조건 변수(Condition Variable)  (0) 2022.01.27
멀티 쓰레드 Lock 구현  (0) 2022.01.26
생성자  (0) 2021.12.27