[CloudNeta] Hands-On LLM Serving 2주차 - 토끼굴: _rlock의 r은 Reentrant의 r인가

이 문서는 2주차 part 1 - 워커 프로세스는 왜 '서 있는가'에서 갈라져 나온 토끼굴입니다. Queue.get()을 분석하다가 변수명만 보고 "_rlock은 Reentrant Lock인 것 같은데"라고 유추했는데, 실제로 추적해 보니 아니었습니다. 결론은 일반 Lock이고, r은 reader의 r입니다.

1단계: multiprocessing/queues.py

Queue.__init__에서 잠금 두 개가 만들어집니다[1].

self._rlock = ctx.Lock()      # 읽기 직렬화용
...
self._wlock = ctx.Lock()      # 쓰기 직렬화용

_rlock과 _wlock이 대구를 이루는 작명입니다. reader lock, writer lock.

2단계: multiprocessing/context.py

ctx는 get_context("fork") 등이 돌려주는 컨텍스트 객체입니다. ForkContext, SpawnContext, ForkServerContext 세 클래스가 있는데(각각 자식 프로세스를 만드는 세 가지 시작 방식), 셋 다 BaseContext를 상속하고 Lock() 메서드는 BaseContext에 한 번만 정의되어 있습니다[2]. 즉 시작 방식은 자식 프로세스를 어떻게 만드느냐만 바꾸고, Lock의 종류에는 영향을 주지 않습니다.

class BaseContext(object):
    def Lock(self):
        '''Returns a non-recursive lock object'''
        from .synchronize import Lock
        return Lock(ctx=self.get_context())

    def RLock(self):
        '''Returns a recursive lock object'''
        from .synchronize import RLock
        return RLock(ctx=self.get_context())

docstring부터 갈립니다. Lock()은 non-recursive, RLock()은 recursive. queues.py가 호출한 것은 ctx.Lock(), 즉 non-recursive 쪽입니다.

3단계: multiprocessing/synchronize.py

최종 실체입니다[2:1]. 둘 다 C 구현체 _multiprocessing.SemLock 위의 얇은 래퍼인데, 첫 인자(kind)가 다릅니다.

class Lock(SemLock):
    def __init__(self, *, ctx):
        SemLock.__init__(self, SEMAPHORE, 1, 1, ctx=ctx)

class RLock(SemLock):
    def __init__(self, *, ctx):
        SemLock.__init__(self, RECURSIVE_MUTEX, 1, 1, ctx=ctx)

재진입 가능 여부는 SEMAPHORE 대 RECURSIVE_MUTEX 플래그가 결정합니다. Queue가 쓰는 것은 SEMAPHORE, 재진입 불가 일반 락입니다.

실증

WSL에서 직접 확인한 결과입니다.

$ python -c "
import multiprocessing as mp
ctx = mp.get_context('fork')
q = ctx.Queue()
print(type(q._rlock))
print(type(ctx.RLock()))
"
<class 'multiprocessing.synchronize.Lock'>
<class 'multiprocessing.synchronize.RLock'>

q._rlock의 타입이 synchronize.Lock으로 나오므로 종결입니다. 동작 차이까지 보고 싶다면 l = ctx.Lock(); l.acquire(); print(l.acquire(timeout=1))이 False(같은 스레드가 두 번 못 잡음)로 나오는 것과, 같은 실험을 ctx.RLock()으로 하면 True가 나오는 것을 비교하면 됩니다.


  1. CPython Lib/multiprocessing/queues.py. Queue.__init__의 _rlock/_wlock 생성. ↩︎

  2. CPython Lib/multiprocessing/context.py 및 CPython Lib/multiprocessing/synchronize.py. BaseContext.Lock()/RLock()의 정의와 SEMAPHORE/RECURSIVE_MUTEX 기반 Lock/RLock 클래스. ↩︎ ↩︎