1. 락(Lock)
공유 데이터(전역 변수와 같은)에 대한 동시 접근을 조절하기 위한 방법.
(1) 락 획득(Lock Acquisition)
락이 걸린 자원에 접근하고자 하는 스레드는 해당 락을 획득해야 한다.
다른 스레드가 락을 획득한 상태라면, 접근하고자 하는 스레드는 락이 해제될 때까지 대기한다.
(2) 락 해제(Lock Release)
락을 획득한 스레드가 작업을 마칠 때 락을 해제한다.
다른 스레드가 해당 공유 자원에 접근할 수 있게 된다.
2. 뮤텍스 (mutex)
상호배타적(Mutual Exclusive) 원리를 이용한 락 기법.
* 상호배타적
한 사건이 발생할 때 다른 사건이 발생하지 않도록 배제하는 것.
(1) 코드 실험 - 동적 배열
mutex m;
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;
}
단순히 동적 배열에 10000개의 요소를 집어넣는 함수를 짜고, 해당 함수를 두 개의 스레드가 접근한다.
- 벡터는 힙 영역에 할당되어 있다(공유 자원이다).
- 벡터의 특성으로 사이즈가 캐퍼시티를 넘어서면 메모리 영역을 이사시켜 확장한다.
- 이전에 할당된 메모리 영역을 해제한다.
즉, 두 개의 스레드가 공유 자원에 대해 경쟁 조건이 발생했다고 볼 수 있다.
벡터가 이사, 확장하는 과정에서 다른 스레드가 그 전의 메모리 영역을 건드렸다고 볼 수 있다.
(2) 코드 실험 - 동적 배열의 캐퍼시티 확보
mutex m;
vector<int> v;
void Push()
{
for (int i = 0; i < 10000; i++)
{
v.push_back(i);
}
}
int main()
{
v.reserve(100000);
thread t1(Push);
thread t2(Push);
t1.join();
t2.join();
cout << v.size() << endl;
}
이번에는 크래시를 방지하기 위해 캐퍼시티를 10만으로 확장한 후에 코드를 실행했다.
- 사이즈가 여전히 캐퍼시티보다 작기 때문에 이사, 확장이 일어나지 않는다.
- 경쟁 조건에 의해 한 스레드가 push back 할 때 다른 스레드가 끼어들게 된다.
따라서, 한 스레드가 push back 한 요소에 다른 스레드가 다음 값을 또 push back 하게 되어 요소를 덮어씌우게 된다.
즉, 결과 값이 2만보다 작아지게 된다.
(3) 코드 실험 - mutex를 사용하여 락 걸기
mutex m;
vector<int> v;
void Push()
{
for (int i = 0; i < 10000; 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;
}
이제는 mutex에 의해 벡터 v에 락이 걸렸기 때문에 push back 전까지 다른 스레드가 대기하게 된다.
이후에 해당 스레드가 종료되면 다른 스레드가 작동한다.
(어떤 스레드가 대기, 작동하는지는 예측할 수 없다)
즉, 한 순간에 하나의 스레드만 작동하게 된다. 이를 동기화(Synchronization)라고 한다.
* 주의
단, 락을 사용하면 다른 스레드들은 대기 상태에 빠지기 때문에 멀티 스레드를 위해서는 적절히 사용하는 것이 중요하다.
또한, lock 이후에는 반드시 unlock을 해야 한다.
3. RAII(Resource Acquisition Is Initilization) 패턴
클래스의 생성자를 통해 리소스를 할당하고, 소멸자를 통해 리소스를 해제한다.
(1) 자원 할당(Resource Acquisition)
클래스의 생성자에서 외부 자원을 할당받는다.
(2) 초기화(Is Initilization)
리소스스을 안전하게 사용하도록 초기화한다.
(3) 자원 해제(Resource Release)
클래스의 소멸자가 호출되면 리소스를 해제한다.
(4) RAII 예제
template <typename T>
class LockGuard
{
public:
LockGuard(T& m) : _mutex(m) // 자원 획득과 초기화
{
_mutex.lock();
}
~LockGuard()
{
_mutex.unlock();
}
private:
T& _mutex;
};
void Push()
{
for (int i = 0; i < 10000; i++)
{
LockGuard<mutex> lockGuard(m);
v.push_back(i);
if (i == 5000)
{
break;
}
}
}
LockGuard의 생성자에서 _mutex를 초기화하고 lock을 걸어준다.
소멸자에서 unlock을 사용한다.
이후에 Push 함수가 동작하면, i가 5000일 때 함수가 종료되며 객체가 해제될 때 소멸자가 호출이 되면서
스레드를 자동으로 unlock하게 된다.
아래 문법을 사용하여 std에서 제공하는 lock_guard 함수를 사용할 수 있다.
std::lock_guard<mutex> lockGuard(m);
(5) std::unique_lock
unique_lock은 lock_guard와 동일하게 동작하지만, 두 번째 매개변수로 값을 넘겨 락이 작동하는 시점을 미룰 수 있다.
std::unique_lock<mutex> uniqueLock(m, std::defer_lock);
uniqueLock.lock();
사용과 동시에 락이 걸리는 lock_guard에 비해 발동 시점을 조정할 수 있다는 장점이 있다.
'서버 프로그래밍 > 멀티 스레드' 카테고리의 다른 글
1-7. 데드락(Deadlock) (0) | 2023.11.24 |
---|---|
1-6. CAS(Compare And Swap), 스핀 락(Spin Lock) (0) | 2023.11.24 |
1-4. 공유 자원과 경쟁 조건 (0) | 2023.11.23 |
1-3. 캐시, CPU 파이프라인, 스레드 경쟁 조건 (1) | 2023.11.23 |
1-2. 멀티 스레드 (0) | 2023.11.23 |