본문 바로가기
게임 서버/[Inflearn_rookiss]올인원_클라&서버 연동

4. Lock 기초

by 헛둘이 2023. 12. 11.

Lock에 대한 개념

- atomic의 경우 특정 변수 하나에 대해서는 thread-safe하게 처리가 가능하지만 특정 범위에 대해서는 thread-safe하게 처리하려면 다른 방법을 강구해야 한다.
 

Mutex (mutual exclusive) - 상호 배타적이라는 의미

- Lock을 사용할 수 있게 해주는 클래스
- thread-safe한 구간을 만들기 위해 해당 구간의 시작부분에 lock을 걸고, 끝 부분에 unlock을 해주면 된다.
 
데이터 영역에 존재하는 vector<int> v의 push_back을 멀티 스레드로 실행하면 어떤 일이 벌어질까?
결과는 크래쉬가 난다. 
- 왜냐하면 push_back의 동작 원리는 데이터를 집어넣고 나서 size가 capacity 이상일 경우 메모리를 다시 할당하고 기존에 있던 데이터들을 복사하는 일련의 과정을 거치는데, 어떤 스레드가 이 작업을 하는 중 다른 스레드가 이 작업을 마치고 데이터를 지워버리는 상황이 발생할 수 있기 때문이다.
-> reserve로 공간을 미리 할당해주니 크래쉬는 해결되었으나 정확한 값이 나오지 않았다.
 
- 그렇다면 atomic<vector<int>>를 해주면 되는가? 이 또한 아니다.
- atomic은 여러 줄에 걸쳐 실행되는 명령어들에 대해 lock_xxx와 같은 명령어로 대체하여 중간 과정에 여러 쓰레드들이 개입하는 것을 방지하는 것인데, vector의 push_back은 내부적으로 힙 메모리 할당 해제, 값이 들어가야 할 자리를 찾고 그 자리에 값을 넣는 일련의 과정들이 있기 때문에 atomic을 사용하더라도 이 과정들을 원자적으로 처리할 수 없다.
-> atomic으로 감싸보니 static_assert failed가 뜨며 실패했다.
'atomic<T>가 복사 가능해야 하고, 복사 생성 가능해야 하고, 이동 생성 가능해야하고 ...'
위에서 언급했듯 vector는 복사나 이동에 복잡한 연산을 수행하기 때문에 원자적이지 않다 라는 결론으로 보여진다.
 

#include <iostream>
#include <thread>
#include <vector>
#include <windows.h>
#include <atomic>
#include <mutex>
using namespace std;

vector<int> v;
mutex m;

void Push()
{
    for (int i = 0; i < 10000; ++i)
    {
        m.lock();
        v.push_back(i);
        m.unlock();
    }
}

int main()
{
    //ios_base::sync_with_stdio(false);
    //cin.tie(NULL);
    //cout.tie(NULL);

    v.reserve(1000000);

    thread t1(Push);
    thread t2(Push);

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

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


    return 0;
}

- mutex의 lock을 사용하니 정상적으로 20000이라는 값이 출력된다.
- 이렇게 한 번에 한 명씩 처리하는 것을 동기화라고 하며, 공유해서 사용하면 안되는 이런 영역을 임계영역이라고 한다.
 
그러면 그냥 공유 데이터마다 락을 잡으면 되는거 아니냐?
- 그렇지 않다. 왜냐하면 lock - 할일 - unlock 패턴으로 이루어져 있을 때 '할 일'에서 많은 시간을 지체하면 그만큼 뒤의 스레드들이 딜레이되기 때문이다.
 
그리고 lock/unlock은 항상 짝을 맞춰줘야 하는데 unlock을 하지 않고 해당 스코프를 탈출하면 그 락은 영원히 풀리지 않게 된다.
 
이 같은 상황을 방지하기 위해 RAII(객체가 스코프를 벗어나면 자동으로 자원을 해제해주는 기법)을 사용하게 된다.
(LockGuard 라는 클래스를 만들고 생성자에서 lock, 소멸자에서 unlock 해주는 원리)
 
이는 C++ 표준에 lock_guard라는 클래스로 구현되어 있고 unique_lock이라는 클래스는 생성자에서 바로 락을 거는게 아니라 락을 거는 시점을 뒤로 미룰 수 있다.
 
atomic과 lock을 알고 있다면 대부분의 공유자원에서 발생하는 문제는 대부분 해결할 수 있다.

'게임 서버 > [Inflearn_rookiss]올인원_클라&서버 연동' 카테고리의 다른 글

6. 데드 락  (0) 2023.12.11
5. 스핀 락  (0) 2023.12.11
3. CPU 파이프라인 및 공유 자원  (0) 2023.12.11
2. 멀티 쓰레드 실습  (0) 2023.12.11
1. 멀티 쓰레드란?  (0) 2023.12.11

댓글