[C++] 자주 쓰이는 lock 종류

RisingJade의 개발기록·2022년 4월 8일
0

Lock

  • C#에는 lock이라는 아주 편한 thread-safe 키워드가 있다. lock(object)를 쓴 뒤 {} 코드 블럭을 감싸주면 그 안에있는 변수나 함수, 실행에 대한 접근은 thread 하나만이 접근할 수 있고 처리하게 된다.
  • 그 C#의 lock과 비슷 한것이 lock_guard다 완전히 같다고 하기엔 좀 다른데. 아무튼 내부적으로 생성자로 lock을 걸었다가 현재 스택프레임이 끝나면서 소멸자가 작동되고 소멸자에 unlock이 호출되는 방식이니 비슷하긴 하다.

C++에서 자주 쓰이는 lock 종류

mutex->lock을 사용

  • 가장 기본적인 lock이다. mutex 변수를 선언하고 lock을 건다.
  • 나중에 반드시 unlock을 적어두어야 하며, 까먹었을시 먹통이 되는 원인이 된다

std::lock_guard를 사용

  • 위의 mutex lock이 사용하기 불편하고 오류에 원인이 되는 경우가 많아서 C#의 lock(object) 처럼 알아서 사용이 끝났으면 unlock을 호출해주는 lock_guard class가 있다
  • 현재 락이 걸린 부분의 작업이 끝나서 현재 스택 프레임이 해제될때 lockguard 내부 소멸자에서 unlock을 해주는 방식이다. 스마트 포인터 내부와 비슷하다.
  • lock_guard의 바리에이션으로 lock 시점을 뒤로 미룰 수 있는 unique_lock 같은 것이 있으며 사용법은 아래 코드에 주석으로 첨부한다.

std::unique_lock 사용

  • lock 잠기는 타이밍을 정할 수 있다.
  • lock을 도중에 풀거나 다시 잡는것이 가능하다.
  • 같은 뮤텍스끼리 데이터 교환이 가능하기도 하는등 여러 지원함수들이 있다.
  • condition variable의 wait의 호출할때 lock
mutex m;
// 선언한 순간부터 lock이 걸린다.
{
  ...
  std::lock_guard<std::mutex> lockGuard(m);
  ...
}
// defer_lock과 같이 호출시uniqueLock.lock()을 호출하기 전까지 lock 걸리는 것을 미룬다.
{
  std::unique_lock<std::mutex> uniqueLock(m, std::defer_lock);

  ...
  uniqueLock.lock()// 실제 락이 걸리는 위치
}

{
	//이렇게 조건없이 뮤텍스를 넘기면 락가드마냥 바로 락걸린다.
	std:unique_lock<std::mutex> uniqueLock(m);    
}

SpinLock

  • spin: 돌다, 빙빙 돌다. 즉, 내가 lock을 가질 때 까지 계속해서 lock 접근을 시도한다.
  • while문에 atomic 구조체 변수를 이용하여 CAS(Compare-And-Swap)류의 접근으로 계속해서 찔러본다.
  • 참고로 C#에는 SpinLock이 이미 System.Thread에 구현되어있다.
  • 사용법
class SpinLock{
private:
	//이름만 봐도 알겠지만 원자성을 지키도록 도와주는 구조체
	atomic<bool> _locked = false;
public:
	void lock()
    {
    	//CAS 방식이용하기 위한 변수
        bool expected = flase;
        bool desired = true;
        
        while(_locked.compare_exchange_strong(expected, desired)== false){
        	expected = false;
        }
    }
    void unlock(){
    	_locked.store(false);
    }
}
_____
//thread에서 접근할 공유변수
int sum = 0;
//스핀 락 타입 설정
SpinLock spinLock;
//Thread에서 작동할 Add 함수
void Add(){
	for(int i =0; i < 1000; i++{
    	// lock_guard를 이용해 spinlock을 건다.
        // lock_guard 생성자에서 SpinLock클래스의 lock이 호출되고 이 lock은
        // while문을 통해 lock권한을 얻을때까지 스핀한다.
    	lock_guard<SpinLock> guard(spinLock);
        sum++;
    }
}

void Sub(){
	for(int i =0; i < 1000; i++{
    	// lock_guard를 이용해 spinlock을 건다.
        // lock_guard 생성자에서 SpinLock클래스의 lock이 호출되고 이 lock은
        // while문을 통해 lock권한을 얻을때까지 스핀한다.
    	lock_guard<SpinLock> guard(spinLock);
        sum--;
    }
}
int main()
{
	Thread t1(Add);
    Thread t2(Sub);
    
    t1.join();
    t2.join();
    
    cout << sum << endl;//결과 0이 나와야 정상
}

추가적으로 보면 좋은 글들

https://woo-dev.tistory.com/164 | 쓰레드의 기본 활용법 with lock and conditional variable

profile
언제나 감사하며 살자!

0개의 댓글