운영체제와 병렬 프로그래밍을 훑어보면 반드시 보게되는 개념인 Semaphore
다중 프로세스 환경에서 공유 자원에 대한 접근을 제어하는 동기화 도구입니다.
Semaphore
는 정수 값을 가진 변수라고 쉽게 말할 수 있습니다.
하지만 Simple is best 라는 말이 있듯, 이 변수가 프로세스 동기화 문제를 해결할때 중요한 역활을 합니다.
가장 기본적인 형태는 0과 1만 사용하는 Binary Semaphore
가 있습니다.
Semaphore 는 정수형 변수 S와 두가지 원자적 연산(P와 V)으로 구성됩니다.
세마포어 값을 감소시킵니다.
임계 구역에 진입하기 전에 수행됩니다.
S > 0이면 S를 1 감소시키고 진입합니다.
S ≤ 0이면 프로세스를 대기시킵니다.
세마포어 값을 증가시킵니다.
임계 구역에서 나올 때 수행됩니다.
S를 1 증가시킵니다.
대기 중인 프로세스가 있으면 하나를 깨웁니다.
세마포어는 두 가지 주요 방식으로 구현될 수 있습니다
pub struct BinarySemaphore {
flag: Mutex<bool>,
cond: Condvar,
}
impl BinarySemaphore {
pub fn new() -> Self {
Self {
flag: Mutex::new(true),
cond: Condvar::new(),
}
}
pub fn acquire(&self) {
let mut flag = self.flag.lock().unwrap();
while !*flag {
flag = self.cond.wait(flag).unwrap();
}
*flag = false;
}
pub fn release(&self) {
let mut flag = self.flag.lock().unwrap();
*flag = true;
self.cond.notify_one();
}
}
pub struct CountingSemaphore {
count: Mutex<usize>,
cond: Condvar,
}
impl CountingSemaphore {
pub fn new(count: usize) -> Self {
Self {
count: Mutex::new(count),
cond: Condvar::new(),
}
}
pub fn acquire(&self) {
let mut count = self.count.lock().unwrap();
while *count == 0{
count = self.cond.wait(count).unwrap();
}
*count -= 1;
}
pub fn release(&self) {
let mut count = self.count.lock().unwrap();
*count += 1;
self.cond.notify_one();
}
}
pub struct StrongSemaphore {
count: Mutex<usize>,
queue: Mutex<VecDeque<Arc<Condvar>>>,
}
impl StrongSemaphore {
pub fn new(count: usize) -> Self {
Self {
count: Mutex::new(count),
queue: Mutex::new(VecDeque::new()),
}
}
pub fn acquire(&self) {
let mut count = self.count.lock().unwrap();
if *count == 0 {
let cvar = Arc::new(Condvar::new());
self.queue.lock().unwrap().push_back(cvar.clone());
while *count == 0 {
count = cvar.wait(count).unwrap();
}
}
}
pub fn release(&self) {
let mut count = self.count.lock().unwrap();
*count += 1;
if let Some(cvar) = self.queue.lock().unwrap().pop_front() {
cvar.notify_one();
}
}
}
pub struct WeakSemaphore {
count: Mutex<usize>,
cond: Condvar,
}
impl WeakSemaphore {
pub fn new(count: usize) -> Self {
Self {
count: Mutex::new(count),
cond: Condvar::new(),
}
}
pub fn acquire(&self) {
let mut count = self.count.lock().unwrap();
while *count == 0 {
count = self.cond.wait(count).unwrap();
}
*count -= 1;
}
pub fn release(&self) {
let mut count = self.count.lock().unwrap();
*count += 1;
self.cond.notify_all();
}
}
BinarySemaphore Timing Results:
Average wait time: 592 ms
Average execution time: 11 ms
CountingSemaphore Timing Results:
Average wait time: 51 ms
Average execution time: 11 ms
StrongSemaphore Timing Results:
Average wait time: 0 ms
Average execution time: 11 ms
WeakSemaphore Timing Results:
Average wait time: 51 ms
Average execution time: 11 ms
실행 시간의 일관성
모든 세마포어 유형에서 평균 실행 시간이 11ms로 동일합니다. 이는 각 작업이 설계대로 약 10ms 동안 실행되었음을 보여줍
BinarySemaphore의 높은 대기 시간
BinarySemaphore가 가장 긴 대기 시간(592ms)을 보였습니다. 이는 한 번에 하나의 작업만 실행할 수 있는 BinarySemaphore의 특성 때문입
StrongSemaphore의 우수한 성능
StrongSemaphore는 0ms의 평균 대기 시간을 보여, 가장 효율적인 성능을 나타냈습니다. 이는 FIFO 방식으로 대기 중인 작업을 관리하는 StrongSemaphore의 특성이 이 테스트 시나리오에 가장 적합했음을 시사합니다.
CountingSemaphore와 WeakSemaphore의 유사한 성능
CountingSemaphore와 WeakSemaphore는 동일한 평균 대기 시간(51ms)을 보였습니다. 이는 두 세마포어가 비슷한 방식으로 작업을 관리하고 있음을 나타냅니다.
이번 테스트를 통해 각 세마포어 유형의 특성과 성능 차이를 명확히 확인할 수 있었습니다.
StrongSemaphore는 FIFO 방식으로 대기 중인 작업을 관리하여 가장 효율적인 성능을 보여주었으며,
이는 공정한 리소스 할당이 필요한 시나리오에 적합합니다.
반면, BinarySemaphore는 한 번에 하나의 작업만 처리할 수 있어 대기 시간이 길어졌습니다. 이는 상호 배제가 필요한 상황에 적합하지만, 성능 요구가 높은 환경에서는 주의가 필요합니다.
CountingSemaphore와 WeakSemaphore는 중간 정도의 성능을 보였으며, 여러 리소스를 동시에 관리해야 하는 경우 유용하게 사용될 수 있습니다. 특히, WeakSemaphore는 순서를 보장하지 않으므로 성능이 중요한 시스템에서 활용할 수 있습니다.
결론적으로, 세마포어를 선택할 때는 애플리케이션의 특성과 요구 사항을 고려해야 합니다. 각 세마포어 유형은 특정 상황에서 강점을 발휘하므로, 적절한 유형을 선택함으로써 시스템의 안정성과 효율성을 극대화할 수 있습니다.
Github: Semaphore