경합이 발생하지만 atomic으로 해결이 안되는 상황

//

#include <iostream>
#include <thread>
#include <atomic>
#include <vector>

using namespace std;

vector<int> vec;

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

int main()
{
	//vec.reserve(20'000); //1.
	thread t1(Push);
	thread t2(Push);

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

	cout << vec.size() << '\\n';

	return 0;
}

Push()는 vector에 1~10’000 까지 push를 하는 함수. 스레드를 두개로 돌리면 크래시가 난다.

벡터 내부에서 배열 공간을 확보하지 못했는데 데이터를 할당하려고 해서 크래시가 나게 된다.

→ 이를 방지하기 위해 (1)주석처럼 reserve를 2만을 해준다면?

이제 크래시는 막았지만, 데이터가 정확히 2만개가 쌓이는것은 보장받지 못한다. 벡터는 데이터를 넣고, 사이즈를 올려가는 방식이기 때문에 push_back하는 명령자체가 atomic하지않고 따라서 atomic 에서봤던대로 데이터를 보장받지 못하게 되는 것.

차라리 크래시가 나서 에러를 찾는 상황이 낫다. 이렇게 데이터가 오염되어 버리면 디버깅하기 더더욱 난해해질 것.

Mutex 객체

따라서 lock을 사용해야한다. mutex역시 atomic이나 thread처럼 여러 환경에서 사용 가능한 객체로 사용.

mutex객체는 재귀동작이 불가능하며, 재귀락을 사용하고 싶다면 std::recursive_mutex 를 사용해야 한다.

//

#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
#include <mutex>

using namespace std;

vector<int> vec;
mutex vecMutex;

void Push()
{
	for (int i = 0; i < 10'000; ++i)
	{
		vecMutex.lock(); //2.
		vec.push_back(i);
		vecMutex.unlock(); 
	}
}

int main()
{
	vec.reserve(20'000); //1.
	thread t1(Push);
	thread t2(Push);

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

	cout << vec.size() << '\\n';

	return 0;
}

lock, unlock은 함수의 리턴, 예외 등으로 인해 실수하는 경우가 많으므로 RAII설계원칙에 따라 설계하는것을 권장한다. 즉, lock_guard같은 객체로 lock/unlock을 자동화 하라는 뜻

(RAII(Resource Acquisition Is Initialization)에서는, 자원 획득과 초기화가 일치해야 한다고 소개한다. 객체가 생성될때 생성자에서 필요 자원을 획득하고, 객체가 소멸할떄 자원을 자동으로 해제하는 패턴. )

lock_guard

std::lock_guard는 C++표준 라이브러리에서 제공하는 RAII기반 뮤텍스 자동관리 객체이다.

생성자에서 뮤텍스(lock)을 획득하고, 소멸자에서 뮤텍스(unlock)을 자동 해제한다.

std::mutex mtx;
// 임계영역
// lock.unlock(); // 필요시 수동 unlock 가능
// lock.lock();   // 다시 lock 가능

std::mutex mtx;
{
    std::lock_guard<std::mutex> lock(mtx); // 생성자에서 lock
    // 임계영역 코드
} // 블록을 벗어나면 소멸자에서 unlock 자동 호출

객체가 살아있으면 계속해서 lock인 상태고, 소멸할때 자동으로 소멸된다.