News Feed

GIL 없는 파이썬을 시작하는 4가지 팁

컨텐츠 정보

  • 조회 415

본문

최근까지 파이썬 쓰레드는 진정한 병렬 처리를 지원하지 않았다. CPU 바운드 작업에서 쓰레드가 서로에게 CPU를 양보했기 때문이다. 파이썬 3.13에 도입된 자유 쓰레드, 또는 ‘GIL이 없는’ 빌드는 C파이썬 인터프리터가 만들어진 이후 가장 큰 아키텍처 변경이다. 이제 여러 파이썬 쓰레드가 자유롭게 병렬로, 최대 속도로 실행될 수 있다. 또한 GIL이 없는 빌드의 도입은 파이썬 프로그램의 스레딩 처리 방식에 관한 많은 전제를 무의미하게 만든다는 측면에서 잠재적으로 가장 파괴적인 변화라고도 할 수 있다.

GIL이 없는 파이썬은 최근까지도 실험 단계였지만 파이썬 3.13 베타 3 릴리스부터 공식 지원된다. GIL이 없는 빌드는 아직 선택 사항이므로 지금이 파이썬의 진정한 병렬 처리에 대한 실험을 시작할 적기라고 할 수 있다. 시작하는 데 도움이 될 4가지 팁을 소개한다.

파이썬 스레딩이 어떤 면에서 도움이 되는지 자문해보기

파이썬 스레딩은 오랫동안 진정한 병렬 처리에는 적합하지 않았기 때문에 지금까지 개발자들은 병렬 처리를 아예 배제해왔다. 이제 GIL 없는 파이썬이 공식적으로 지원되므로 자신의 사용 사례에 대해 병렬 처리를 진지하게 고려할 수 있게 됐다.

언어를 불문하고 쓰레드는 모든 작업에 대해 ‘분할 후 정복하는’ 접근 방식을 취한다. 병렬화하기가 매우 쉬운 모든 작업은 쓰레드와 잘 맞는다. 그러나 모든 문제가 여러 쓰레드에 균등하게 분할되지는 않는다. 문제에 따라서는 스레딩으로 얻은 이득을 다른 부분에서 다시 잃을 수도 있다.

예를 들어 많은 파일을 쓰는 작업을 한다고 가정해 보자. 각 작업을 자체 쓰레드에서 처리한다 해도 각 작업이 파일을 쓴다면 효과가 떨어진다. 파일 쓰기는 본질적으로 직렬 작업이기 때문이다. 이 경우 더 나은 접근 방식은 작업을 여러 쓰레드로 분할하고 디스크에 쓰기 작업을 위한 쓰레드는 하나만 두는 것이다. 각 작업이 완료되면 디스크 쓰기 작업에 작업을 전송한다. 이렇게 하면 작업이 서로를 막지 않으며, 그 작업 자체도 파일 쓰기로 인해 막히지 않는다.

쓰레드에 사용 가능한 가장 높은 수준의 추상화 사용

파이썬 스레딩에는 사용 가능한 여러 수준의 추상화 계층이 있다.

  • threading.Thread를 사용해 직접 쓰레드를 생성하고 관리할 수 있다. 다만 각 쓰레드의 수명을 직접 관리해야 하고, 쓰레드가 종료될 때까지 기다렸다가 결과를 가져오는 부분도 개발자가 직접 처리해야 한다. 이 방식은 쓰레드 실행이 종료될 다른 작업이 차단되는 프로그램에는 괜찮지만 고수준 작업에는 적합하지 않다.
  • 그보다 더 높은 수준의 추상화로 concurrent.futures.ThreadPoolExecutor가 있다. 이 방식에서는 수신되는 요청에 응답할 쓰레드 풀이 생성된다(개수 설정 가능). 작업을 풀에 제출하고 편할 때 결과를 가져올 수 있으므로 작업 완료를 기다리느라 프로그램이 멈출 일이 없다.
  • 위의 두 가지 추상화는 모두 훨씬 더 낮은 수준의 _thread 모듈 위에 구축된 추상화다. _thread는 운영체제 수준의 쓰레드 처리를 위한 추상화다. 추상화 수준이 높을수록 GIL 없는 파이썬의 효과가 제대로 발휘될 가능성이 높다.

일반적인 규칙에 따르면 파이썬 내에서 사용하는 쓰레드(확장 모듈과 같은 외부 소스가 아닌)는 파이썬 내에서 생성해야 한다. 이론적으로는 C파이썬 확장에서 쓰레드를 생성하고 인터프리터에 등록할 수 있지만 인터프리터가 어차피 잘 하는 일을 굳이 중복해서 할 필요는 없다.

ProcessPoolExecutor를 추상화 계층으로 이미 사용하고 있다면 GIL 없는 빌드에서 손쉽게 ThreadPoolExecutor로 교체할 수 있다. 이 둘은 동일한 인터페이스를 제공하므로 import 부분만 편집하면 된다.

사이썬 모듈의 쓰레드 안전성 확보

GIL 없는 파이썬을 위해 넘어야 할 가장 큰 장애물은 C 언어로 작성되거나 C 호환 인터페이스가 있는 사이썬 확장이 GIL 없는 새로운 설계에 따르도록 하는 것이다.

파이썬에서 C 확장을 제작하는 데 사용되는 핵심 툴인 사이썬은 C파이썬 런타임의 변경 사항을 면밀히 추적한다. 사이썬 유지보수 팀은 최근 C파이썬의 GIL 없는 빌드에 대한 지원을 추가했지만 코드의 쓰레드 안전성은 여전히 개발자가 직접 확인해야 한다. 특히 파이썬 객체를 다룰 때는 쓰레드 안전성이 필요하다.

코드의 쓰레드 안전성을 이미 확신한다면 사이썬 모듈에 지시문을 추가하고 GIL 없는 빌드에서 테스트할 수 있다.

# cython: freethreading_compatible = True

이렇게 하면 해당 모듈은 GIL 없는 빌드와 호환되는 것으로 표시된다. GIL 없는 빌드를 실행한 다음 이 표시가 되지 않은 모듈을 가져오는 경우 인터프리터는 안전을 위해 GIL을 자동으로 다시 활성화한다.

기존 사이썬 모듈에 쓰레드 안전성을 더 추가하려는 경우 사이썬에 추가된 몇 가지 툴을 사용하면 쉽게 할 수 있다.

  • 크리티컬 섹션(critical section) : 컨텍스트 관리자로, 파이썬 객체를 받아 컨텍스트 블록 시간 동안 이 객체를 위한 C파이썬 크리티컬 섹션, 또는 로컬 잠금을 생성한다. 일반적으로 클래스 메서드에 대한 함수 데코레이터로도 사용 가능하다(클래스 인스턴스에 잠금 적용). 크리티컬 섹션은 데드락을 자동으로 방지하지만 그 대신 크리티컬 섹션 전반에서 잠금이 지속적으로 유지된다는 보장을 하지 못한다. 즉, 다른 객체에 잠금이 필요한 경우 잠금을 해제한 후 다시 획득하는 경우가 발생할 수 있다.
  • 파이뮤텍스(PyMutex) 잠금 : 더 강력한 잠금으로, 개발자가 명시적으로 획득하고 해제한다. 참고로 GIL 없는 빌드가 아닌 빌드에서 사용하는 경우(예를 들어 하위호환성을 위해) PyMutex 잠금 동안 GIL을 다시 획득하면 데드락이 발생할 위험이 있다.

이터레이터 또는 프레임 책체를 쓰레드 간에 공유하지

객체 중에는 내부 상태에 쓰레드 안전성이 없어 쓰레드 간에 공유하면 안 되는 객체가 있다. 대표적인 두 가지 예가 이터레이터와 프레임 객체다.

파이썬의 이터레이터 객체는 내부 상태를 기반으로 객체 스트림을 생성한다. 예를 들어 제너레이터는 이터레이터 객체를 생성하는 일반적인 방법이다.

이터레이터를 생성했다면 쓰레드에서 다른 쓰레드로 이터레이터를 전달해서는 안 된다. 이터레이터에 의해 생성되는 객체는 쓰레드 안전하다면 공유해도 되지만, 이터레이터 자체는 공유하면 안 된다. 예를 들어 문자열의 문자를 하나씩 생성하는 이터레이터를 만들려면 다음과 같이 할 수 있다.

data = "abcdefg"d_iter = iter(data)item = next(d_iter)item2 = next(d_iter)# ... etc.

이 예제에서 d_iter는 이터레이터 객체다. data와 item(또는 item2 등)은 쓰레드 간에 공유할 수 있지만 d_iter 자체는 쓰레드 간에 공유할 수 없다. 공유할 경우 내부 상태가 손상될 가능성이 높기 때문이다.

파이썬프레임 객체는 프로그램이 실행되는 특정 시점의 상태에 대한 정보를 포함하고 있다. 이 객체는 다양한 용도로 활용되지만, 특히 오류 상황이 발생했을 때 파이썬의 디버깅 메커니즘이 프로그램의 상세 정보를 생성하는 데 사용된다.

그러나 프레임 객체는 쓰레드 안전(thread-safe)**하지 않다. 프로그램 내부에서 sys.current_frames()를 통해 프레임 객체에 접근할 경우, 프리 쓰레드(free-threaded) 빌드 환경에서는 문제가 발생할 수 있다. 반면, inspect.currentframe()이나 sys._getframe()을 사용하는 방식은 프레임 객체를 쓰레드 간에 공유하지 않는 한 상대적으로 안전하다.

따라서 디버깅 목적이나 실행 상태 추적 시 프레임 객체를 사용할 때에는, 쓰레드 환경과 접근 방식에 따라 안전성을 충분히 고려해야 한다.
dl-itworldkorea@foundryco.com

관련자료

댓글 0
등록된 댓글이 없습니다.
Member Rank