서버/멀티스레드

3. Lock

광란의슈가슈가룬 2024. 7. 11. 03:18
#include <thread>

vector<int> v;

void Push()
{
	for (int i = 0; i < 10000; i++)
	{
		v.push_back(i);
	}
}

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

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

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

벡터에 1만번 push하는 코드이다.

위와 같이 코드를 작성하면 출력값이 20000이 될 것 같지만 예상과 달리 오류가 난다.

 

지금까지 우리가 STL에서 사용하던 자료구조 등은 멀티스레드 환경에서 작동하지 않는다고 생각하는게 편하다.

 

다시 본문으로 돌아와서 벡터의 경우 동적 배열로 용량이 가득 차면 더 큰 캐퍼시티를 할당받고 기존의 데이터를 복사한 후 원래 메모리의 데이터를 삭제한다.

여기서 문제가 발생하게 된다. 캐퍼시티를 늘리고 복사하고 삭제하는 과정에서 멀티스레드의 경우는 이러한 행위가 끝날 때까지 기다려주지 않는다.

그렇기 때문에 데이터를 2번 삭제하게 된다던가 하는 문제가 생길 수 있는 것이다.

그렇다면 reserve함수를 이용해 처음부터 벡터의 크기를 2만개로 할당해 주면 되는게 아닌가?

#include <thread>

vector<int> v;

void Push()
{
	for (int i = 0; i < 10000; i++)
	{
		v.push_back(i);
	}
}

int main()
{
	v.reserve(20000);

	thread t1(Push);
	thread t2(Push);

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

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

코드를 위와 같이 수정할 경우 에러는 생기지 않지만 아래와 같이 데이터가 분실되는 문제가 생긴다.

출력 결과

이는 멀티스레드 환경에서 데이터가 완전히 입력되기 전의 상태를 가져와 다른 스레드가 같은 위치에 데이터를 입력하기 때문에 발생한다.

 

이런 현상을 방지하기 위해서 한 번에 한 개의 스레드만 접근할 수 있도록 해야하는데 이를 Lock이라고 한다.

#include <mutex>

Lock의 경우 위와 같이 mutex 헤더를 추가하여 사용할 수 있다. mutex는 Mutual Exclusion의 줄임말로 상호배제를 의미한다.

 

#include <thread>
#include <mutex>

vector<int> v;
mutex m;

void Push()
{
	for (int i = 0; i < 10000; i++)
	{
		m.lock();
		v.push_back(i);
		m.unlock();
	}
}

int main()
{
	v.reserve(20000);

	thread t1(Push);
	thread t2(Push);

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

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

mutex를 이용해 코드를 수정한 모습이다. mutex는 자물쇠라고 생각하면 된다.

화장실에 들어가면 다른 사람이 들어오지 못하게 문을 잠그는 것처럼 다른 스레드가 접근하지 못하도록 lock()을 통해 잠그고 사용이 끝나면 unlock()으로 잠금을 해제한다.

출력 결과

정상적으로 출력이 되는 것을 볼 수 있다.

lock()과 unlock() 사이에서는 하나의 스레드만 접근할 수 있기 때문에 싱글스레드와 같이 작동한다고도 볼 수 있다.

 

물론 lock()과 unlock()의 과정이 있기 때문에 일반적인 상황보다 느리게 동작한다.

 

그럼 여기서 의문점이 생긴다.

#include <thread>
#include <mutex>

vector<int> v;
mutex m;

void Push()
{
	for (int i = 0; i < 10000; i++)
	{
		m.lock();
		m.lock();
		v.push_back(i);
		m.unlock();
		m.unlock();
	}
}

int main()
{
	v.reserve(20000);

	thread t1(Push);
	thread t2(Push);

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

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

이런식으로 중복해서(재귀적으로) lock을 걸 수 있을까?

mutex같은 경우에는 재귀적으로 호출은 불가능하다.

하지만 재귀적으로 lock을 걸 수 있는 recursive_mutex가 존재한다.

 

그렇다면 lock을 굳이 여러번 걸어야 되는 이유가 있을까?

위 코드는 단순히 push만 하는 코드지만 코드가 복잡해지면 내부에서 다른 함수를 호출할 수도 있고 그 함수에서 또 lock을 걸 수 있기 때문에 중복 lock을 허용하는 것이 유지보수에 유리하다.

'서버 > 멀티스레드' 카테고리의 다른 글

6. Sleep  (0) 2024.07.17
5. SpinLock  (1) 2024.07.14
4. DeadLock  (0) 2024.07.11
2. Atomic  (0) 2024.07.10
1. 스레드 생성  (0) 2024.07.10