#include "pch.h"
#include <iostream>
#include "CorePch.h"
#include <atomic>
#include <mutex>
#include <windows.h>
#include <future>
#include "ConcurrentQueue.h"
#include "ConcurrentStack.h"
LockQueue<int32> q;
LockStack<int32> s;
void Push()
{
while (true)
{
int32 value = rand() % 100;
q.Push(value);
this_thread::sleep_for(10ms);
}
}
void Pop()
{
while (true)
{
int32 data = 0;
if (q.TryPop(OUT data))
cout << data << endl;
}
}
int main()
{
thread t1(Push);
thread t2(Pop);
thread t3(Pop);
t1.join();
t2.join();
t3.join();
}
//cpp
#include "pch.h"
#include "ConcurrentStack.h"
//header
#pragma once
#include <mutex>
template<typename T>
class LockStack
{
public:
LockStack() { }
LockStack(const LockStack&) = delete;
LockStack& operator=(const LockStack&) = delete;
void Push(T value)
{
lock_guard<mutex> lock(_mutex);
_stack.push(std::move(value));
_condVar.notify_one();
}
bool TryPop(T& value)
{
lock_guard<mutex> lock(_mutex);
if (_stack.empty())
return false;
// empty -> top -> pop
value = std::move(_stack.top());
_stack.pop();
return true;
}
void WaitPop(T& value)
{
unique_lock<mutex> lock(_mutex);
_condVar.wait(lock, [this] { return _stack.empty() == false; });
value = std::move(_stack.top());
_stack.pop();
}
private:
stack<T> _stack;
mutex _mutex;
condition_variable _condVar;
};
//cpp
#include "pch.h"
#include "ConcurrentQueue.h"
//header
#pragma once
#include <mutex>
template<typename T>
class LockQueue
{
public:
LockQueue() { }
LockQueue(const LockQueue&) = delete;
LockQueue& operator=(const LockQueue&) = delete;
void Push(T value)
{
lock_guard<mutex> lock(_mutex);
_queue.push(std::move(value));
_condVar.notify_one();
}
bool TryPop(T& value)
{
lock_guard<mutex> lock(_mutex);
if (_queue.empty())
return false;
value = std::move(_queue.front());
_queue.pop();
return true;
}
void WaitPop(T& value)
{
unique_lock<mutex> lock(_mutex);
_condVar.wait(lock, [this] { return _queue.empty() == false; });
value = std::move(_queue.front());
_queue.pop();
}
private:
queue<T> _queue;
mutex _mutex;
condition_variable _condVar;
};
Main 코드를 보게 되면, LockQueue로 q를 선언하였다. 그런데 만약에 여기서 정의한 LockQueue로 선언을 한 것이 아니라 기본적인 Queue를 이용하게 됐다면, 이 상황에서는 멀티쓰레드가 pop과 push를 하면서 크래쉬가 발생하게 된다.
이 크래쉬가 나는 것을 막기위해서 LockStack과 LockQueue를 구현하였다. 여기서 사실상 둘의 코드는 비슷하여 하나만 살펴보면 되는데, LockQueue를 기준으로 살펴보겠다.
먼저 LockQueue에는
LockQueue(const LockQueue&) = delete;
LockQueue& operator=(const LockQueue&) = delete;
다음과 같은 코드가 있다. 이 부분은, 멀티쓰레드 환경 자체가 복사 연산에 취약하기 때문에 다음과 같은 코드로 만약에 LockQueue를 복사를 해도 무용지물이 되게끔 해준다.
그다음은 Push를 살펴보면
void Push(T value)
{
lock_guard<mutex> lock(_mutex);
_queue.push(std::move(value));
_condVar.notify_one();
}
lock_guard를 통해서 lock을 잡은 후, queue에 값을 추가 해준다. 이 때, move(value)를 이용하는데, 여기서 move는 l-value를 r-value로 추출하기 위해 사용된다. 그러고나서 condition_variable의 notify_one() 메소드를 통하여 이제 lock이 풀릴 것이고 다음 스케쥴러가 가져가라는 이벤트를 발생한다.
다음은 TryPop이다.
bool TryPop(T& value)
{
lock_guard<mutex> lock(_mutex);
if (_queue.empty())
return false;
value = std::move(_queue.front());
_queue.pop();
return true;
}
TryPop의 경우 bool 값을 return 하고, value값을 참조형으로 가져와서 외부에 value를 통해 값을 확인 할 수 있게 해주었다. 코드 내부를 살펴 보면, 먼저 lock을 걸고, 만약에 큐가 비어있는 경우에는 false를 return 해주고, 그렇지 않다면 value에 queue에 앞에 있는 값을 넣어주고 queue_pop을 통해 앞에 있는 값을 제거해준다. 그 이후 true를 리턴해준다. 이 경우에는 Condition_Variable을 사용하지 않았기 때문에 해당 함수가 실행이 됐다면, lock을 잡기위해 무한으로 반복을 하는 SpinLock이 일어날 것이다.
다음은 WaitPop이다.
void WaitPop(T& value)
{
unique_lock<mutex> lock(_mutex);
_condVar.wait(lock, [this] { return _queue.empty() == false; });
value = std::move(_queue.front());
_queue.pop();
}
WaitPop의 경우에는 condition_variable을 사용하였다. 내부의 코드를 살펴보면 먼저 눈에 띄는 것은 unique_lock을 사용한 점이다 condition_variable은 wait 메소드에 첫번째 인수로 unique_lock을 잡기 때문에 이를 unique_lock으로 lock을 걸어주었다. 그래서 여기서 WaitPop이 lock을 잡게되면 wait 메소드의 람다식의 return 값이 참인지 거짓인지 즉 큐가 비었는지 아니었는지를 보고 만약에 비어있다면 lock 풀어주고 대기상태로 다시 들어갑니다. 그리고 다시 push를 통해서 notify가 들어오게되면 다시 lock을 잡고, 조건을 확인하여 만족하면 value 값을 변경해줍니다.
cf) queue나 stack이 top또는 front를 통해서 값을 확인하고 pop을 하는 이유는 멀티쓰레드의 경우 이러한 작업들이 한꺼번에 진행이 된다 할 때 DeadLock이 생겨날 수 있는 염려가 있기 때문에 C++에서는 이렇듯 먼저 값을 참조해서 가져오고 그리고 pop을 통해서 값을 제거하게끔 설계되어있다.
cf2) template을 이용한 메소드의 경우 헤더와 cpp파일로 쪼개어서 관리 할 수 없으며, header 파일에서 전체를 작성하여야 한다.