Server/C++

Lock

Juzdalua 2024. 7. 29. 13:32

공유 데이터와 멀티스레드, STL 컨테이너의 문제점

멀티스레드 환경에서 STL 컨테이너는 올바르게 작동하지 않는다.

아래 코드 vector 컨테이너로 에러 이유를 확인해보자.

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>
#include <atomic>

vector<int32> v;

void Push() {
	for (int32 i = 0; i < 10'000; i++) {
		v.push_back(i);
	}
}

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

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

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

v 벡터의 capacity가 꽉 찬 상황이라고 가정하면 rezise가 일어나고 기본 벡터보다 사이즈가 더 큰 벡터를 생성한 후 기존 데이터를 복사한다. 그리고 기존의 벡터를 메모리에서 지우려고 할 것이다.

만약 t1 스레드에서 메모리를 지운 상태에서 동일하게 t2 스레드가 메모리를 지우려는 절차에 진입한 순간 double free 문제가 발생하게 된다.

 

v.reserve(20000);

만약 메인 스레드가 시작할 때, 2만개의 공간을 할당하고 시작하면 double free 크래시는 발생하지 않지만 2만개의 데이터가 삽입되지 않는다. 이유는 다음과 같다.

인덱스 i에 스레드 t1, t2가 동시에 접근한다고 가정해보자.

t1이 데이터를 삽입하는 순간 t2 또한 데이터를 삽입할 수 있기 때문에 각 스레드는 한 번씩 삽입 작업을 진행하지만, 결과적으로 하나의 데이터만 삽입되는 순간이 생길 수 있다.

 

atomic<vector<int32>> v;

atomic 템플릿을 사용한다면 atomic 구조체의 기능을 사용할 뿐이지, 벡터의 기능을 사용할 수 없어 문제가 해결되지 않는다.


Lock

mutex.lock() 함수가 실행되면, 해당 스레드에서 unlock 작업이 실행되기 전까지 다른 스레드가 접근하면 대기시키는 역할을 수행한다.

즉, 싱글스레드로 작업하는 효과이다.

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>
#include <mutex>

vector<int32> v;
mutex m;

void Push() {
	for (int32 i = 0; i < 10'000; i++) {
		m.lock();
		v.push_back(i);
		m.unlock();
	}
}

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

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

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

 

직접 코드로 lock()과 unlock()을 수행하는 것은 안전하지 않은 방법이다.

RAII 패턴을 활용하여 wrapper class를 사용하는 방법이 있다.

 

#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <thread>
#include <mutex>

vector<int32> v;
mutex m;

// RAII (Resource Acquisition is Initialization)
template <typename T>
class LockGuard {
public:
	LockGuard(T& m) {
		_mutex = &m;
		_mutex->lock();
	}
	~LockGuard() {
		_mutex->unlock();
	}

private:
	T* _mutex;
};

void Push() {
	for (int32 i = 0; i < 10'000; i++) {
		//LockGuard<mutex> lockGuard(m);
		lock_guard<mutex> lockGuard(m); // std 표준에도 존재하는 클래스 템플릿이다.
		v.push_back(i);
	}
}

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

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

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

lock_guard 클래스 템플릿은 객체를 생성하는 부하가 존재하지만 안전하게 코드를 작성하는 방법이다.

lock_guard 클래스 템플릿은 범위에 따라서 생성되고 소멸된다. -> { }

 

unique_lock<mutex> uniqueLock(m, defer_lock);
uniqueLock.lock();

unique_lock은 lock_guard보다 세부적인 옵션을 추가할 수 있다.

lock() 시점을 생성자가 아닌 원하는 위치에서 수행할 수 있다.

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

효율적인 락 구현 방법  (0) 2024.07.29
데드락 ( Dead Lock )  (0) 2024.07.29
쓰레드 (Thread)와 Atomic  (0) 2024.07.29
STL) Map과 Set  (0) 2024.07.28
STL) Vector와 List - iterator  (0) 2024.07.27