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 |