본문 바로가기

Development/Diary

[개발일기] 이 코드에서 Semaphore 사용하는 이유가 무엇인가요?

Semaphore?

세마포어(Semaphore)는 컴퓨터 시스템 내에서 다수의 프로세스 간의 활동을 조정하기 위해 사용되는 일반적인 변수입니다. 세마포어는 상호 배제(Mutual Exclusion)를 강제하고 경쟁 조건(Race Condition)을 피하며 프로세스 간 동기화를 구현하는 데 사용됩니다.

세마포어를 사용하는 과정에는 "wait"와 "signal" 두 가지 연산이 포함됩니다. wait 연산은 세마포어의 값을 감소시키며, signal 연산은 세마포어의 값을 증가시킵니다. 세마포어의 값이 0인 경우, wait 연산을 수행하는 프로세스는 다른 프로세스가 signal 연산을 수행할 때까지 블록됩니다.

세마포어는 임계 영역(Critical Section)을 구현하는 데 사용되며, 이는 코드의 일부를 오직 하나의 프로세스만 실행해야 하는 영역입니다. 세마포어를 사용하여 프로세스는 공유 메모리나 I/O 장치와 같은 공유 리소스에 대한 접근을 조정할 수 있습니다.

프로세스가 세마포어에 대한 대기 연산을 수행할 때, 연산은 세마포어의 값이 0보다 큰지 확인합니다. 그렇다면 세마포어의 값을 감소시키고 프로세스를 계속 실행시킵니다. 그렇지 않으면 세마포어에 프로세스를 블록시킵니다. 세마포어에 대한 신호 연산은 세마포어에 대기 중인 프로세스가 있다면 해당 프로세스를 활성화하거나, 그렇지 않으면 세마포어의 값을 1 증가시킵니다. 이러한 의미로 세마포어는 카운팅 세마포어라고도 불립니다. 세마포어의 초기 값은 대기 연산을 통과할 수 있는 프로세스 수를 결정합니다.

아래 그림은 간단한 세마포어 구현입니다.


P 함수에서 세마포어 값이 0이면 대기하고 있다가 0이 아니면 s 값을 -1 합니다. V 함수는 s 값을 +1 합니다.

이를 통해 어떤 프로세스는 P함수를 실행하여 Semaphore를 얻고, Critical Section에 들어갈 코드를 수행한 다음 다 끝나면 V함수를 실행시켜 Semaphore를 반납합니다.

위 사진은 이진 세마포어(Binary Semaphore)의 상황을 보여줍니다.
두 개의 프로세스 P1과 P2가 있고 세마포어 s가 1로 초기화되었다고 가정해봅시다. 이제 P1이 임계 영역에 들어가면 세마포어 s의 값이 0이 됩니다. 그런데 만약 P2가 자신의 임계 영역에 들어가려고 한다면, 이는 s가 0보다 클 때까지 대기해야 함을 의미합니다. 이러한 상황은 P1이 자신의 임계 영역을 마치고 세마포어 s에 대한 V(증가) 연산을 호출할 때만 발생할 수 있습니다.
이렇게 함으로써 상호 배제가 달성됩니다.

Semaphore? Lock?

Lock은 주로 스레드나 프로세스 간의 상호 배제를 구현하기 위해 사용됩니다. Lock은 특정 코드 블록에 대한 접근을 제한하여 동시에 한 스레드만 해당 영역에 접근할 수 있도록 합니다.

Lock과 위에서 구현한 이진 세마포어(Binary Semaphore)의 차이는 주로 동일한 리소스에 접근하려는 여러 프로세스가 있는 경우에 두드러집니다.

두 동기화 메커니즘은 같은 시간에 리소스에 하나의 스레드만 접근하도록 허용합니다. 그러나 Lock은 하나의 프로세스 내에서만 접근을 제한하는 반면, 이진 세마포어는 여러 프로세스 간의 접근을 제한할 수 있습니다.

따라서 단일 프로세스 내에서는 락과 이진 세마포어의 동작이 동일합니다. 두 경우 모두 하나의 스레드만 리소스에 접근할 수 있습니다.

그러나 여러 프로세스 간에는 동작이 다릅니다. 이진 세마포어는 한 번에 하나의 프로세스만 특정 리소스에 액세스할 수 있지만, Lock은 여러 프로세스에 리소스 액세스를 제공할 수 있습니다 (다만 한 번에 한 프로세스 내의 하나의 스레드만이 특정 시점에 액세스할 수 있음).

상호 배제만 필요한 경우에는 락을 사용하는 것이 더 간단할 수 있습니다. 반면에 자원 관리나 다양한 스레드 또는 프로세스 간의 협력이 필요한 경우에는 세마포어를 사용하는 것이 유용합니다.

이 코드에서는 왜 Semaphore를?

아래 코드는 python asyncio 에서 Semaphore를 사용하는 예제입니다.

sem = asyncio.Semaphore(10)

# ... later
await sem.acquire()
try:
    # work with shared resource
finally:
    sem.release()

asyncio.Semaphore(10)은 내부 카운터의 초깃값을 제공합니다.

sem.acquire() 는 세마포어를 얻습니다.

sem.release()는 세마포어를 반납하고 내부 카운터를 1 증가시킵니다. 세마포어를 얻기 위해 대기하는 태스크를 깨울 수 있습니다.

 

제가 이해를 못했던 코드는 아래 입니다.
https://github.com/lablup/backend.ai/blob/6a3745a62224e0700d22f134df17445ba801677b/src/ai/backend/agent/agent.py#L1594C18-L1594C18

        """
        Create a new kernel.
        """
        if throttle_sema is None:
            # make a local semaphore
            throttle_sema = asyncio.Semaphore(1)
        async with throttle_sema:
            if not restarting:
                await self.produce_event(
                    KernelPreparingEvent(kernel_id, session_id),
                )

세마포어는 자원을 보호하거나 acquire 하는 쪽과 release 하는 쪽의 짝이 맞아야 하는데 한군데밖에 없고 보호하는 자원이 무엇인지.. 왜 이 부분에서 async with throttle_sema로 Critical Section 을 만들 필요가 있었는지..가 궁금했어요.

 

제가 들었던 답변은 아래입니다.

> Semaphore는 N개 이상의 코루틴이 동시에 동작하는걸 막습니다.  만약 동시에 생성해야 하는 컨테이너가 너무 많다면, 컨테이너 실행 환경인 Docker가 특정 조건 하에서 "context deadline exceeded" 오류로 실패할 수 있습니다. 그래서 create_kernels() RPC 호출마다 커널 생성 작업의 최대 동시성을 제한해야 합니다.

 

그래서 새로운 커널을 생성할때 비동기적으로 여러 코루틴이 실행할 때, 실행할 수 있는 코루틴의 수를 제한을 했던 것이었습니다.

Semaphore에 역할에 대해 다시 복습하게 되는 질문이었습니다. 

 

 

참고자료

https://www.geeksforgeeks.org/semaphores-in-process-synchronization/

 

Semaphores in Process Synchronization - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org