mutex
이번에 직접 구현해 볼 것은
mutex이며 mutex를 SpinLock 형태로 구현하는 것이 목표이다
아래의 코드를 통해서 mutex의 기능과 역할을 복습해 보자
int sum = 0;
mutex m;
void Add()
{
for (int32 i = 0; i < 100'000; i++)
{
lock_guard<mutex> guard(m);
sum++;
}
}
void Sub()
{
for (int32 i = 0; i < 100'000; i++)
{
lock_guard<mutex> guard(m);
sum--;
}
}
int main()
{
std::thread t1(Add);
std::thread t2(Sub);
t1.join();
t2.join();
cout << sum << endl;
}
SpinLock 구현 - 미완성
아래의 코드처럼 단순히 while문을 통해서
기다리게 구현하면 되지 않겠나 싶지만 실제 결과는 오류가 발생한다
class SpinLock
{
public:
void lock()
{
while (_locked)
{
}
_locked = true;
}
void unlock()
{
_locked = false;
}
private:
bool _locked = false;
};
int sum = 0;
//mutex m;
SpinLock m;
void Add()
{
for (int32 i = 0; i < 100'000; i++)
{
//lock_guard<mutex> guard(m);
lock_guard<SpinLock> guard(m);
sum++;
}
}
void Sub()
{
for (int32 i = 0; i < 100'000; i++)
{
//lock_guard<mutex> guard(m);
lock_guard<SpinLock> guard(m);
sum--;
}
}
int main()
{
std::thread t1(Add);
std::thread t2(Sub);
t1.join();
t2.join();
cout << sum << endl;
}
C++ volatile
위의 오류가 발생한 이유는 아니지만
여기서 volatile 키워드를 알고 갈 필요가 있다
c++ volatile은 c#과 java와는 다르다
c++ volatile은 단순히 최적화를 하지 말아 달라고 하는 것이다
c#과 java는 해당 기능뿐 아니라 다양한 기능을 제공한다 (ex. 캐시메모리 읽지 않고 RAM에서 읽도록,...)
최적화의 여부에 따른 결과의 차이는 아래의 코드를 통해서 알 수 있다
int a = 1;
a = 2;
a = 3;
// 최적화가 되어서
// int a = 3;
volatile int b = 1;
b = 2;
b = 3;
// volatile 키워드를 통해 모든 줄이 실행
bool flag1 = true;
while (flag1)
{
}
// 최적화가 되어서
// while(true)
volatile bool flag2 = true;
while (flag2)
{
}
// volatile 키워드를 통해 flag2를 true로 대입하고 while문에서 비교
SpinLock 구현 - 문제
처음 만든 SpinLock에서 오류가 발생한 이유는
동시에 두 스레드가 들어가 버리는 문제가 발생하기 때문이다
따라서 들어가는 동작 자체가 한 번에 일어나야 한다
다시 말해서,
lock() 내부의
while문과 _locked = true;
두 과정으로 이루어져 있는 것을 하나의 과정으로 진행해야 한다는 것이다
class SpinLock
{
public:
void lock()
{
// 스레드가 여기에 있을 수도 있고
while (_locked)
{
// 스레드가 여기에 있을 수도 있고
}
// 스레드가 여기에 있을 수도 있고
_locked = true;
// 스레드가 여기에 있을 수도 있으면서 lock에 두개의 스레드가 존재하는 상태가 발생
}
void unlock()
{
_locked = false;
}
private:
volatile bool _locked = false;
};
SpinLock 구현 - 해결
위의 문제를 해결하기 위해서는 atomic과 atomic 기능을 활용하면 된다
참고로 atomic에는 volatile 기능도 포함된다
이처럼 atomic 변수를 활용해
atomic 기능의 compare_exchange_strong(expected, desired)을 활용하면 완전히 해결할 수 있다
여기서
expected는 기대하는 값
desired는 성공하면 바뀌어야 하는 값을 의미한다
따라서, _locked 값이 expected 값이 되었는지 지속적으로 확인을 해서
성공하면, true를 반환하도록 동작한다
compare_exchange_strong(expected, desired)의 정확한 내부 동작은 아래와 같다
if (_locked == expected)
{
expected = _locked;
_locked = desired;
return true;
}
else
{
expected = _locked;
return false;
}
추가적으로,
위의 코드를 통해 알 수 있듯이
compare_exchange_strong의 첫 번째 인자인 expected에
_locked의 값이 참조로 매번 들어가게 되는 것을 알 수 있다
그래서 구현을 할 때는 while문 내부에서 expected = false;로 계속 바꿔줘야 한다
추가적으로,
atomic 변수에 bool값을 그냥 넣게 되면
bool 타입으로 혼동할 수 있으니
store 기능을 사용하고 있다
class SpinLock
{
public:
void lock()
{
bool expected = false;
bool desired = true;
while (_locked.compare_exchange_strong(expected, desired) == false)
{
expected = false;
}
//while (_locked)
//{
//}
//_locked = true;
}
void unlock()
{
//_locked = false;
_locked.store(false);
}
private:
atomic<bool> _locked = false;
};
int sum = 0;
//mutex m;
SpinLock m;
void Add()
{
for (int32 i = 0; i < 100'000; i++)
{
//lock_guard<mutex> guard(m);
lock_guard<SpinLock> guard(m);
sum++;
}
}
void Sub()
{
for (int32 i = 0; i < 100'000; i++)
{
//lock_guard<mutex> guard(m);
lock_guard<SpinLock> guard(m);
sum--;
}
}
int main()
{
std::thread t1(Add);
std::thread t2(Sub);
t1.join();
t2.join();
cout << sum << endl;
}
'게임 서버' 카테고리의 다른 글
[게임서버 섹션2 Note] Event (0) | 2024.12.30 |
---|---|
[게임서버 섹션2 Note] Sleep (0) | 2024.12.29 |
[게임서버 섹션2 Note] Lock 구현 이론 (0) | 2024.12.29 |
[게임서버 섹션2 Note] DeadLock (0) | 2024.12.29 |
[게임서버 섹션2 Note] Lock 기초 (0) | 2024.12.28 |