semaphore (C++20)
mutex와 비슷하면서도 다르다. 리소스 제한을 위해 사용하거나 signal을 보낼 때도 사용할 수 있다.
기본 개념을 보면 세마포어는 내부에 카운터를 가지고 있다.
그림으로 잠시 확인해보면, 가운데 숫자 2는 초기 카운트이며 아무리 많은 쓰레드가 리소스를 획득하려해도 동시에 2개의 쓰레드만 해당 리소스로 접근이 가능하다.

acquire는 카운터가 1개 줄어들고 release는 카운터가 1개 증가한다.
만약, 카운트가 0인 상태에서 acquire 시도하면 해당 쓰레드는 wait 즉,block 상태가 된다.
나중에 다른 쓰레드가 release를 통해 카운터를 하나 증가시킨다면 wait상태에 있던 쓰레드가 이를 가져가서 다시 카운터를 0으로 만들고 계속해서 일을 수행한다.
C++에서는 카운팅 베이스의 std::counting_semaphore와 바이너리로 동작하는 std::binary_semaphore 두 가지를 제공한다. 둘 다 같다고 보면 되는데 cppreference를 보면 using binary_semaphore = std::counting_semaphore<1>로 선언되어 있다. 이는 바이너리 세마포어는 카운팅 세마포어와 같고 카운터의 최대값이 1인 세마포어라는 의미이다.
Example Code
#include <iostream>
#include <thread>
#include <chrono>
#include <semaphore>
// <최대값> (초기값)
std::counting_semaphore<2> sp(2);
void fn()
{
// C++ 20부터 추가가 되었고 추가된 gcc 버전은 11
sp.acquire();
std::cout << "semaphore region" << std::endl;
sp.release();
}
int main()
{
std::thread t1(fn);
std::thread t2(fn);
std::thread t3(fn);
t1.join();
t2.join();
t3.join();
return 0;
}

세마포어는 C++20부터 도입이 되었기 때문에 다음과 같이 gcc 컴파일을 위해서 gcc 버전 11 옵션을 주어 컴파일한다.

위의 코드는 정상적으로 동작하지만 정말 세마포어가 지정한 Counter인 2개까지만 지원이 되는지 확인하기 위해 release()를 없애본다.
#include <iostream>
#include <thread>
#include <chrono>
#include <semaphore>
// <최대값> (초기값)
std::counting_semaphore<2> sp(2);
void fn()
{
// C++ 20부터 추가가 되었고 추가된 gcc 버전은 11
sp.acquire(); // wait
std::cout << "semaphore region" << std::endl;
//sp.release();
}
int main()
{
std::thread t1(fn);
std::thread t2(fn);
std::thread t3(fn);
t1.join();
t2.join();
t3.join();
return 0;
}

결과를 보면 2개의 thread는 세마포어를 acquire한 후 메시지를 출력하였지만 마지막 thread는 wait 상태로 기다리고 있는 것을 볼 수 있다. 즉, 아무리 많은 thread가 동시에 돌아가도 Counter 이상의 thread는 wait 상태로 대기하게 된다.
Example Code
class RscManager
{
public:
void createRsc() // 리소스를 만들 때 acquire()
{
// 리소스를 만들 때는 최대 2개를 제한하고 있기 때문에 2개까지만 만들어진다.
mSp.acquire();
// 실제 구현에는 mutex 등의 추가 코드가 필요하다.
}
void removeRsc() // 리소스를 지울 때 release()
{
mSp.release();
}
private:
std::counting_semaphore<2> mSp{2};
};
지금은 단편적으로 사용하고 있는데 똑같은 개념을 리소스에서 사용하는 위의 예제를 확인해본다. 현재는 간단하게 나타냈지만 실제 구현에서는 mutex같은 추가 코드가 필요할 것이다.
세마포어와 뮤텍스가 다른 점은 뮤텍스는 뮤텍스 lock을 하는 thread와 unlock을 하는 thread가 같아야한다. 하지만 세마포어는 하나의 thread가 acquire하고 다른 thread가 release를 하는 것이 가능하다.이러한 특성 때문에 세마포어는 Signal을 보내는 용도로 사용할 수 있는 것이다.
예를 들어, wait thread와 signal thread가 있고 세마포어 초기 카운트 값이 0인 경우

초기 카운트가 0이기 때문에 wait thread는 acquire를 시도하면서 block상태로 넘어가게 된다.

대기 상태의 wait thread를 깨우기 위해서는 signal을 보내야한다. signal은 signal thread에서 release 함수를 통해 카운트를 1 증가시킨다.

그럼 카운트가 1 감소하고 block 상태였던 wait thread가 깨어나면서 자신의 일을 계속 수행한다.
Example Code
#include <iostream>
#include <thread>
#include <chrono>
#include <semaphore>
std::counting_semaphore<10> sp(0);
void waitFn()
{
std::cout << "waiting" << std::endl;
sp.acquire();
std::cout << "rerun" << std::endl;
}
void signalFn()
{
std::cout << "signal" << std::endl;
sp.release();
}
int main()
{
std::thread waitT(waitFn);
std::thread signalT(signalFn);
waitT.join();
signalT.join();
return 0;
}

예상한대로 결과가 출력되었다. 물론 thread의 실행 순서에 따라 출력 결과가 바뀔 수 있다. 하지만 이것은 signal을 먼저 받느냐 waiting이 먼저 시작되느냐의 차이일뿐이다.
마지막으로 세마포어를 사용할 때 꼭 주의해야할 점이 있는데 프로세스의 진행 중에 thread 간에 shared data가 있고 데이터 레이스가 발생할 수 있다면 꼭 뮤텍스로 보호해주어야한다.
'C++ > Thread' 카테고리의 다른 글
[Thread] std::latch (0) | 2022.08.09 |
---|---|
[Thread] Producer-Consumer 패턴 (0) | 2022.07.26 |
[Thread] Condition Variable (0) | 2022.07.23 |
[Thread] scoped static 초기화 (0) | 2022.07.19 |
[Thread] std::call_once (0) | 2022.07.18 |