1. 락의 필요성 및 문제 이해

멀티스레딩 환경에서 여러 스레드가 공유 자원(예: 동적 메모리, STL 컨테이너 등)에 동시 접근하면 데이터 경합(Data Race) 문제가 발생할 수 있습니다. 이를 방지하기 위해 락(lock)을 사용하여 특정 코드 영역에 대해 한 번에 하나의 스레드만 접근할 수 있도록 제어합니다.


2. 주요 개념

2.1 아토믹(Atomic)의 한계

// 모든 것을 아토믹으로 바꿀 수 있을까? -> NO
  • 문제점:
    • atomic은 하나의 변수에 대해 원자적 연산을 보장하지만, 여러 변수 또는 STL 컨테이너 같은 복잡한 데이터 구조에 대해서는 문제가 발생할 수 있습니다.
    • 원자적으로 처리되지 않는 연산들이 있다면 atomic만으로 해결할 수 없습니다.
    • atomic으로 모든 연산을 처리하려 하면 성능 저하가 발생합니다.

2.2 STL 컨테이너의 문제

// STL 컨테이너들은 멀티스레드 환경에서 동시 접근 시 크래시가 발생
vector<int> v;
v.push_back(1);  // 멀티스레드 환경에서 안전하지 않음
  • 문제점:
    • STL 컨테이너(예: std::vector)는 멀티스레드 안전하지 않습니다.
    • 여러 스레드가 동시에 push_back 같은 함수를 호출하면 내부 데이터가 손상되거나 크래시가 발생합니다.
  • 이유:
    • STL 컨테이너는 힙 메모리에 데이터를 저장합니다.
    • 힙 메모리는 모든 스레드가 공유하므로, 한 스레드가 메모리를 수정하는 동안 다른 스레드가 접근하면 문제가 발생합니다.

2.3 메모리 재할당 문제

// 벡터의 크기를 미리 reserve 하면 메모리 재할당은 방지 가능
v.reserve(100000);
  • std::vector는 크기가 초과될 경우 내부적으로 새로운 메모리를 할당하고 데이터를 복사합니다(재할당).
  • 문제점:
    • 여러 스레드가 동시에 벡터에 데이터를 추가하면 메모리 재할당 중 경합이 발생하여 데이터가 손상될 수 있습니다.
  • 해결책:
    • reserve를 사용하여 벡터 크기를 미리 확장하면 재할당 빈도를 줄일 수 있습니다.
    • 그러나 여전히 동시 접근 문제는 해결되지 않으므로 을 사용해야 합니다.

3. 락(lock)이란?

  • 정의: 특정 코드 블록에 대해 한 번에 하나의 스레드만 접근하도록 제한하는 메커니즘.
  • 목적: 공유 자원을 보호하여 데이터 경합 및 손상을 방지.
  • 키워드:
    • mutex(Mutual Exclusive): 상호 배타적 접근을 보장하는 락.

4. 코드 및 주석 분석

4.1 기본 mutex 사용

mutex m;  // 뮤텍스 선언
vector<int> v;

void Push()
{
    for (int i = 0; i < 10000; i++)
    {
        m.lock();       // 자원 잠금
        v.push_back(i); // 공유 자원 접근
        m.unlock();     // 자원 해제
    }
}
  • m.lock():
    • 뮤텍스를 사용하여 Push 함수 내부의 코드 블록을 잠급니다.
    • 다른 스레드가 v.push_back에 접근하지 못하도록 제한합니다.
  • m.unlock():
    • 뮤텍스를 해제하여 다른 스레드가 접근할 수 있도록 합니다.
  • 문제점:
    • 락과 언락 쌍을 잊어버리면 교착 상태(Deadlock) 또는 잘못된 동작이 발생할 수 있습니다.

4.2 락 가드(LockGuard) 사용

template<typename T>
class LockGuard
{
public:
    LockGuard(T& m) : _mutex(m) { _mutex.lock(); }
    ~LockGuard() { _mutex.unlock(); }
private:
    T& _mutex;
};
  • RAII 패턴:
    • 생성자에서 lock(), 소멸자에서 unlock()을 호출하여 락과 언락을 자동화합니다.
    • 예외가 발생해도 소멸자가 호출되어 자원이 안전하게 해제됩니다.
  • 사용법:
    void Push()
    {
        for (int i = 0; i < 10000; i++)
        {
            LockGuard<mutex> lockGuard(m); // 락 자동 관리
            v.push_back(i);
        }
    }

4.3 표준 락 가드 사용 (std::lock_guard)

void Push()
{
    for (int i = 0; i < 10000; i++)
    {
        std::lock_guard<mutex> lockGuard(m); // 표준 락 가드 사용
        v.push_back(i);
    }
}
  • std::lock_guard:
    • RAII 패턴을 구현한 표준 라이브러리 클래스.
    • 락과 언락을 안전하게 관리.

4.4 유니크 락(Unique Lock)

void Push()
{
    for (int i = 0; i < 10000; i++)
    {
        std::unique_lock<mutex> uniqueLock(m, std::defer_lock); // 락을 뒤로 미룸
        uniqueLock.lock();                                     // 특정 시점에서 락
        v.push_back(i);
        uniqueLock.unlock();                                   // 명시적으로 해제
    }
}
  • std::unique_lock:
    • 락 시점을 명시적으로 제어할 수 있습니다.
    • 예외 발생 시에도 락이 안전하게 해제됩니다.
  • std::defer_lock:
    • 락을 생성자에서 바로 수행하지 않고, 나중에 명시적으로 lock()을 호출합니다.
  • 유용성:
    • 특정 조건에 따라 락을 잠그거나 해제해야 할 때 유용.

4.5 메인 함수

int main()
{
    v.reserve(100000);  // 벡터 크기 사전 예약
    thread t1(Push);    // 스레드 1 실행
    thread t2(Push);    // 스레드 2 실행
    thread t3(Push);    // 스레드 3 실행

    t1.join();  // 스레드 1 종료 대기
    t2.join();  // 스레드 2 종료 대기
    t3.join();  // 스레드 3 종료 대기

    cout << v.size() << endl; // 벡터 크기 출력
}
  • 벡터 크기 출력:
    • 모든 스레드가 종료된 후 벡터의 크기를 출력합니다.
    • v.size()는 정확히 10000 * 3 = 30000이어야 합니다.

profile
李家네_공부방

0개의 댓글