![]()
“repr 한 줄”이 락이 되는 순간: free-threaded에서 functools가 PyDict_Next를 critical section으로 감싼 이유
meta_description: free-threaded 환경에서 dict 순회는 ‘그냥 읽기’가 아니라 동시성 계약의 일부가 된다. gh-145446(PR #145487)는 functools 내부에서 PyDict_Next로 kwargs 등을 순회하는 루프를 critical section으로 감싸고, 에러 처리/정리 순서를 바꿔 경계면을 명확히 했다. 어떤 루프가 왜 바뀌었는지, 실무에서 확장 모듈/FFI의 dict 순회를 어떻게 짜야 덜 흔들리는지 정리한다. meta_keywords: functools, free-threaded, critical section, CPython, PyDict_Next, kwargs, partial, repr, dict iteration, error handling, cleanup, lock, GIL-less, 동시성, data race, refcount, C extension, FFI, 안정성 meta_robots: index,follow
free-threaded 쪽 변경을 보다 보면, 가끔 “이게 왜 여기서?” 싶은 지점이 나온다. 이번 gh-145446 / PR #145487이 그렇다.
사람들이 보통 위험하다고 생각하는 건 네이티브 확장이나 I/O 같은 데다. 그런데 패치가 들어간 곳은 functools다. 더 정확히는 kwargs 같은 dict를 순회하는 루프.
이게 재미있는 이유는, 이 경로가 대부분의 서비스에서 ‘겉보기 무해한 곳’으로 쓰이기 때문이다.
- 로그에서 객체를 찍는
repr - 캐시 키를 만들기 위해 kwargs를 정렬/나열하는 코드
- 에러 메시지에 함수/인자를 조금 더 친절하게 담는 코드
이런 건 기능이 아니라 운영이다. 운영 코드가 동시성과 만나는 순간, “그냥 출력”이 “잠깐의 락”이 된다.
이번 PR은 “큰 기능 추가”가 아니라, kwargs 딕셔너리를 순회하는 짧은 구간에 락 경계를 그어준 패치다. 어디를 감싸고, 에러가 날 때 어떻게 빠져나오는지가 포인트다.
![]()
free-threaded에서 dict 순회가 민감해지는 이유: “읽기만 한다”가 더 이상 핑계가 아니다
파이썬에서 dict 순회는 익숙하다. for k, v in d.items() 같은 걸 매일 쓴다.
CPython 내부에선 PyDict_Next 같은 API로 dict를 순회한다. 문제는 free-threaded에서 dict가 다른 스레드에서 바뀔 수 있다는 점이다. 바뀌는 순간에 순회가 어떤 계약으로 동작해야 하는지(그리고 도구가 그걸 어떻게 검증하는지)는 구현 디테일이 아니라 운영 디테일이 된다.
PR이 선택한 답은 “순회 구간을 critical section으로 감싸자”다. 즉, kwargs 등을 순회해 문자열을 만들거나 값을 확인하는 동안은, 다른 스레드의 변경과 충돌하지 않게 범위를 정한다.
중요한 건 락을 “많이” 잡는 게 아니라, 락을 “어디서” 잡는지가 코드에 남는다는 점이다.
겉보기 무해한 호출이 어떻게 발생하나: partial / repr / kwargs는 운영 코드에서 자주 터진다
아래 코드는 “이게 버그다”가 아니라, 어떤 경로에서 functools와 kwargs 순회가 자주 호출되는지 감각을 만드는 예시다.
from functools import partial
def handler(user_id, **kwargs):
# 운영 코드에서 흔히 하는 일: 로그/메트릭 태그
# repr이 호출되면 내부적으로 kwargs를 읽어 문자열을 만들 수 있다.
return f"user={user_id} extra={kwargs!r}"
def main():
f = partial(handler, 42, feature="A", rollout=0.1)
# 어디선가 디버그/로그가 f를 찍는 순간 repr이 돈다.
print(f)
# kwargs를 다른 경로로 다시 넘기는 코드도 흔하다.
print(f(feature="B"))
if __name__ == "__main__":
main()
이런 코드에서 사람은 보통 “이건 읽기만 하잖아”라고 생각한다. free-threaded에서는 그 ‘읽기만’이 어떤 범위로 보호되는지가 실제 동작의 일부가 된다.
PR의 변화(모양): PyDict_Next 루프를 critical section으로 감싸고, 에러 처리 순서를 고정한다
실제 변경은 _functoolsmodule.c 안의 특정 루프다. kwargs 딕셔너리를 순회해 무언가(주로 표현/가공)를 만들 때 PyDict_Next가 돌고, 그 구간이 critical section으로 감싸진다.
(의사코드)
이 PR을 보면서 딱 체크할 곳은 두 군데다.
Modules/_functoolsmodule.c에서partial_vectorcall()안의PyDict_Next(...)루프가 dict를 critical section으로 감싸도록 바뀐다. diff에서Py_BEGIN_CRITICAL_SECTION(keyword_dict);가 while 루프 앞에 들어가고, 끝나고 나서Py_END_CRITICAL_SECTION();가 추가된 걸 보면 된다.- 같은 파일의
partial_repr()에서도PyDict_Next루프를 감싸는데, 여기서는 에러가 나면 break→unlock→goto done으로 빠지게 구조를 바꿔 놓았다. “에러로 일찍 리턴할 때도 락이 남지 않게”를 코드로 박아둔 셈이다.
그러니까 기능이 달라졌다기보다, free-threaded에서 kwargs dict를 읽는 짧은 구간에 “여기부터 여기까지는 건드리지 마”라는 선을 그어준 패치다.
이걸 실무에 어떻게 번역하나: dict 순회는 “락 + 정리”가 한 쌍이다
대부분의 서비스 코드는 C를 직접 건드리지 않는다. 그래도 이 PR이 주는 힌트는 있다.
- dict를 순회하는 동안 다른 스레드가 그 dict를 바꿀 수 있다면
- 순회 코드는 ‘읽기’가 아니라 ‘공유 자원 접근’이다
- 공유 자원 접근은 unlock/cleanup과 한 세트로 설계해야 한다
이게 바뀌면, ‘친절한 로그’가 시스템을 흔드는 방식이 달라진다.
예를 들어 free-threaded에서 특정 객체의 repr이 자주 호출되고, 그 repr 안에서 dict를 순회한다면, 비용은 CPU보다 락 경합으로 보일 수 있다. 그래서 운영 코드는 repr을 호출하는 빈도 자체를 조절하는 게 더 효과적일 때도 있다.
이 글의 목적은 “repr을 쓰지 말자”가 아니다. repr을 호출하는 위치가 생각보다 많고, 그중 일부는 free-threaded에서 더 민감해질 수 있다는 감각을 갖자는 쪽이다.
그리고 이 PR을 읽으면서 제일 현실적으로 떠오른 장면이 하나 있다. 운영에서 “원인을 찾기 위해 로그를 늘리는 순간”이다.
에러가 나서 kwargs를 더 찍고, 객체를 더 찍는다. 그때 repr은 거의 자동으로 따라온다. 문제는 그 repr이 어디까지 들어가서 무엇을 순회하는지(그리고 그 사이에 다른 스레드가 무엇을 바꿀 수 있는지)를 보통은 생각하지 않는다는 점이다.
그래서 나는 free-threaded 같은 환경을 염두에 둘 때, 관찰/표현 코드를 ‘안전한 코드’로 취급하지 않는다. 핫패스에서 전체 repr을 만들기 시작하면, CPU보다 경합이 먼저 보일 수 있다. 가능하면 필요한 키 몇 개만 뽑아 메시지를 만들고, 파이썬 레벨이라면 kwargs = dict(kwargs)처럼 스냅샷을 떠서 이후엔 그 사본만 만지는 편이 낫다.
그리고 무엇보다, 예외가 나는 순간의 경로를 먼저 본다. 정상 경로는 다들 조심해서 짜는데, return/goto가 튀어나오는 순간 락/정리가 빠지기 쉽다.
이번 PR은 “dict를 순회하다가 예외가 나도, critical section은 남기지 않는다”를 구조로 박아둔 패치처럼 보인다.
한 줄로 말하면, free-threaded에서는 “출력/표현”도 공유 자원 접근이다. 그래서 이 경계는 한번 그어두면 계속 이득을 본다.
참고자료
- CPython PR #145487 — gh-145446: wrap PyDict_Next loop in a critical section (free-threaded)
- PR diff
- Merge commit 17eb0354ff3110b27f811343c2d4b3c85f2685d5
- Modules/_functoolsmodule.c @ 17eb0354ff3110b27f811343c2d4b3c85f2685d5
이미지 크레딧/라이선스
- Hands on the keyboard (Unsplash).jpg — Puk Khantho / CC0
댓글
댓글 쓰기