고급 동기화 기법

김 무무 ㅣ 2024. 10. 16. 11:53

1. 세마포어(Semaphore)

동시에 리소스에 접근할 수 있는 스레드의 수를 제한하는 동기화 기법

 

특징:

  - 사용 가능한 리소스의 수를 나타내는 정수 값을 가짐

  - P(획득) 연산과 V(해제) 연산을 제공

  - 카운팅 세마포어와 이진 세마포어로 구분됨 (C++20 이후)

 

 

1.1. 카운팅 세마포어 vs 이진 세마포어

 

값의 범위:

  - 카운팅 세마포어: 0 이상의 정수 값을 가질 수 있음

  - 이진 세마포어: 0 또는 1의 두 가지 값만 가질 수 있음

 

용도:

  - 카운팅 세마포어: 여러 개의 리소스를 관리할 때 사용

  - 이진 세마포어: 하나의 리소스에 대한 접근을 제어할 때 사용되어 뮤텍스와 유사하게 사용 가능

 

동작 방식:

  -  카운팅 세마포어: 여러 프로세스가 동시에 리소스에 접근 가능

  -  이진 세마포어: 한 번에 하나의 프로세스만 리소스에 접근 가능

 

구현 복잡성:

  -  카운팅 세마포어: 상대적으로 더 복잡한 구현 필요

  -  이진 세마포어: 구현이 단순함

 

 

1.2. 사용 예시

semaphore는 C++20 이후부터 사용 가능하다.

#include <iostream>
#include <thread>
#include <semaphore>
#include <vector>
std::counting_semaphore<3> pool(3); // 3개의 리소스를 관리하는 세마포어

void Worker(int id) {
    std::cout << "Worker " << id << " 리소스 획득 시도\n";
    pool.acquire();
    std::cout << "Worker " << id << " 리소스 획득\n";

    // 작업
    std::this_thread::sleep_for(std::chrono::seconds(2));

    std::cout << "Worker " << id << " 리소스 반환\n";
    pool.release();
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 5; ++i)
        threads.emplace_back(Worker, i);

    for (auto& t : threads)
        t.join();
}

 

위 예시에서 최대 3개의 스레드가 동시에 리소스를 획득할 수 있다.

 

 

2. 모니터(Monitor)

객체 지향 개념을 동기화에 적용한 동기화 구조

공유 자원에 대한 상호배제(Mutual exclusion)와 조건 동기화(Condition synchronization)를 제공한다.

 

특징:

  - 데이터와 해당 데이터에 접근하는 프로시저를 하나의 단위로 캡슐화

  - Mutex를 취득하지 못한 스레드는 waiting queue에서 대기

  - 한 번에 하나의 스레드만 모니터 내의 메서드를 실행 가능

  - Condition Variable을 사용하여 스레드 간 통신을 지원

 

 

ex)

#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <condition_variable>
#include <vector>
using namespace std;

class Monitor {
private:
    queue<int> q;
    mutex mtx;
    condition_variable cv;
    const size_t maxSize;

public:
    Monitor(size_t size) : maxSize(size) {}

    void Enqueue(int value) {
        unique_lock<mutex> lock(mtx);
        cv.wait(lock, [this]() { return q.size() < maxSize; });
        q.push(value);
        cout << "Enqueued: " << value << " / Queue size: " << q.size() << endl;
        cv.notify_one();
    }

    int Dequeue() {
        unique_lock<mutex> lock(mtx);
        cv.wait(lock, [this]() { return !q.empty(); });
        int value = q.front();
        q.pop();
        cout << "Dequeued: " << value << " / Queue size: " << q.size() << endl;
        cv.notify_one();
        return value;
    }
};

int main() {
    Monitor monitorQueue(100);  // 최대 100개의 아이템을 담을 수 있는 큐
    vector<thread> producers;
    vector<thread> consumers;

    for (int i = 0; i < 10; i++) {
        producers.emplace_back([&monitorQueue, i]() {
            for (int j = 0; j < 10; j++)
                monitorQueue.Enqueue(i * 10 + j);
            });
    }

    for (int i = 0; i < 10; i++) {
        consumers.emplace_back([&monitorQueue]() {
            for (int j = 0; j < 10; j++)
                monitorQueue.Dequeue();
            });
    }

    for (auto& p : producers)
        p.join();

    for (auto& c : consumers)
        c.join();
}

 

모니터 클래스를 만들고 lock_guard를 사용해 각 메서드에서 자동으로 Mutex를 잠그고 해제하도록 만들었다.

 

 

3. 읽기-쓰기 락(Read-Write Lock)

여러 스레드가 동시에 읽기 작업을 수행할 수 있지만, 쓰기 작업은 독점적으로 수행되어야 하는 상황에서 사용

 

특징:

  - Read Lock: 여러 스레드가 동시에 획득 가능

  - Write Lock: 한 번에 하나의 스레드만 획득 가능

  - 읽기 작업이 많고 쓰기 작업이 적은 경우에 사용
  - 읽기 작업이 계속되면 쓰기 작업이 기아 상태에 빠질 수 있다.

 

 

ex)

shared_mutex는 C++17 이상부터 실행 가능하다.

#include <iostream>
#include <shared_mutex>
#include <thread>
#include <vector>
using namespace std;

class ReadWriteLock {
    mutable shared_mutex mutex;
    int data = 0;

public:
    void Read() const {
        {
            shared_lock<shared_mutex> lock(mutex);
            cout << "Read data : " << data << endl;
        }
        this_thread::sleep_for(chrono::milliseconds(100));
    }

    void Write(int newData) {
        unique_lock<shared_mutex> lock(mutex);
        data = newData;
        cout << "Write data : " << data << endl;
        this_thread::sleep_for(chrono::milliseconds(100));
    }
};

int main() {
    ReadWriteLock rwLock;
    vector<thread> threads;
    // 읽기 스레드 생성
    for (int i = 0; i < 5; i++) {
        threads.emplace_back([&rwLock]() {
            for (int j = 0; j < 3; j++) {
                rwLock.Read();
            }
            });
    }

    // 쓰기 스레드 생성
    threads.emplace_back([&rwLock]() {
        for (int j = 1; j <= 3; j++) {
            rwLock.Write(j * 10);
        }
        });

    for (auto& t : threads)
        t.join();
}

 

읽기 작업은 shared_lock을 사용해 동시에 수행될 수 있고, 쓰기 작업은 unique_lock을 사용해 독점적으로 수행된다.

 

 

4. 비교

Semaphore:

  - 가장 기본적인 형태로, 저수준의 동기화에 유리

  - 주로 생산자-소비자 문제 해결에 사용

 

Monitor:

  - 공유 자원을 내부에 포함하고, 연산들을 하나의 단위로 묶는 객체지향 방식의 동기화

  - 한번에 하나의 스레드만 실행되어야 하거나 여러 스레드가 협업할 때 사용

 

Read-Write Lock:

  - 동시에 수행 가능한 읽기 작업과 배타적인 쓰기 작업을 구분해서 관리

  - 읽기 작업이 많고 쓰기 작업이 적은 경우에 사용

'멀티스레딩' 카테고리의 다른 글

스레드 풀(Thread Pool)  (0) 2024.10.17
Condition Variable  (0) 2024.10.16
C++ 비동기 프로그래밍  (0) 2024.10.15
스핀락(Spinlock)  (0) 2024.10.12
데드락(Deadlock)  (1) 2024.10.07