c++ 11 이후 에선 #include <thread>, std::thread(쓰레드함수) 로 Windows 환경에 종속적이지 않은 멀티 쓰레드를 구현할 수 있다.
- 엔트리 포인트 : 쓰레드 함수
- 자식 쓰레드 t가 종료되기 전에 메인 쓰레드가 먼저 종료되면 에러가 뜸.
- 디버그 시 각 쓰레드별 실행중 문장 확인 가능.
쓰레드 주요 함수들
- 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 |