본문 바로가기
프로그래밍 언어

Python Thread는 블로킹I/O에 사용한다. GIL이란?

by 내기록 2023. 8. 20.
반응형

목차 LIST

     

     

    핵심

    • 파이썬 쓰레드는 전역 인터프리터 잠금(GIL : Global Interpreter Lock) 때문에 여러 CPU 코어에서 병렬로 바이트코드를 실행할 수 없음
    • GIL이 있지만 쓰레드는 여러 작업을 동시에 하는 것처럼 보여주기 쉬우므로 유용한 부분이 있다.
    • 여러 시스템 콜을 병렬로 수행할 때 파이썬 쓰레드를 사용하자. 이렇게 하면 CPU 연산과 같은 계산 작업을 하는 동안에도 병렬로 I/O 작업을 수행할 수 있다.

     

     

    쓰레드를 블로킹 I/O용으로 사용하고 병렬화용으로는 사용하지 말자

     

    CPython은 파이썬 프로그램을 두 단계로 실행한다.

    1) 소스코드를 바이트코드(byte code)로 파싱하고 컴파일한다.

    2) 스택 기반 인터프리터로 바이트코드를 실행한다.

    참고) CPython : 파이썬 코드를 실제로 실행하는 프로그램으로 C언어로 작성되었다.
    다른 여러 파이썬 구현체가 있지만(Jython, IronPython, PyPy), CPython은 가장 표준적이고 널리 사용되는 버전이다.
    즉, 우리가 파이썬이라고 할 때 생각하는 것은 CPython을 말하는 것과 같다.

    바이트코드 인터프리터는 파이썬 프로그램이 실행되는 동안 지속되고, 일관성 있는 상태를 유지한다. 파이썬은 전역 인터프리터 잠금(GIL : Global Interpreter Lock)이라는 메커니즘으로 일관성을 유지한다.

    GIL은 쓰레드 안정성 문제를 해결하기 위한 메커니즘으로 한 번에 하나의 쓰레드만 파이썬 바이트코드를 실행할 수 있도록 제한한다. 이는 멀티코어 CPU를 사용하는 여러 쓰레드 환경에서는 성능 문제를 일으킬 수 있지만, CPython의 내부 구현을 단순화하고 안정성을 유지하기 위한 선택이다.

     

    https://www.datacamp.com/tutorial/python-global-interpreter-lock

     

    GIL은 본질적으로 상호 배제 잠금(mutex)이며 CPython이 선점형 멀티스레딩의 영향을 받지 않게 막아준다. 선점형 멀티스레딩(preemptive multithreading)은 한 스레드가 다른 스레드를 인터럽트(차단)해서 프로그램의 제어를 얻는 것을 말한다. 이 인터럽트가 예상치 못한 시간에 발생되면 인터프리터의 상태가 망가지는데 GIL은 이런 인터럽트를 막아주며 모든 바이트코드 명령어가 CPython 구현과 C 확장 모듈에서 올바르게 동작함을 보장한다.

     

    GIL의 큰 부작용은 C++이나 자바로 작성된 프로그램에서 여러 스레드를 실행한다는 것은 프로그램이 동시에 여러 CPU 코어를 사용함을 의미한다. 파이썬도 멀티쓰레드를 지원하지만 GIL은 한 번에 한 스레드만 실행하게 하기 때문에 병렬 연산을 해야 하거나 파이썬 프로그램의 속도를 높여야 하는 상황에서는 적합하지 않다.

     

     

    순서대로 작업을 실행한 코드 vs 쓰레드로 작업을 실행한 코드

    샘플 코드는 첨부하지 않겠지만, 테스트를 해보면 순서대로 작업을 실행한 코드보다 쓰레드를 사용했을 때 더 많은 시간이 걸린다.

    예) 1.040 seconds vs 1.061 seconds

    작업이 4개고 쓰레드를 4개 생성했다면 실행 순서를 조율하는 부담을 감안했을 때 4배 미만의 속도 향상을 기대할 것이다. 그리고 이 코드를 듀얼코어 머신에서 실행한다면 2배 정도의 속도 향상을 기대할 것이다. 하지만 활용 가능한 CPU가 여러개일지라도 성능이 더 나빠진다는 것을 알 수 있다. GIL이 표준 CPython 인터프리터에서 실행하는 프로그램에 미치는 영향이다.

     

    그럼에도 불구하고 파이썬은 왜 쓰레드를 지원하는가?

    이유 1) 멀티쓰레드를 이용하면 프로그램이 동시에 여러 작업을 하는 것처럼 보이기 용이하다. 즉, 마치 병렬로 실행하는 것처럼 해야하는 일을 파이썬에게 맡길 수 있다. GIL 때문에 한 번에 한 쓰레드만 진행하지만, CPython은 파이썬 쓰레드가 어느 정도 공평되게 실행됨을 보장한다.

     

    이유2) 특정 유형의 시스템 호출을 수행할 때 발생하는 블로킹 I/O를 다루기 위함이다.

    블로킹 I/O는 파일 읽기/쓰기, 네트워크 요청, 데이터베이스 쿼리 등이 있다. 쓰레드는 운영체제가 이런 요청에 응답하는 데 드는 시간을 프로그램과 분리하므로 블로킹 I/O를 처리할 때 유용하다.

     

    이렇게 블로킹I/O가 있는 경우 쓰레드를 사용하면 병렬 처리 시간이 훨씬 짧아진다. GIL은 파이썬 코드가 병렬로 실행되지 못하게 하지만 시스템 호출에는 이런 부정적인 영향이 없기 때문이다. 이는 파이썬 쓰레드가 시스템 콜을 생성하기 전에 GIL을 풀고 시스템 콜이 끝나는대로 다시 GIL을 얻기 때문이다.

     

    쓰레드 이외에도 asyncio처럼 블로킹I/O를 다루는 다양한 수단이 있다. 하지만 이런 옵션을 선택하면 실행 모델에 맞춰 코드를 재작성해야하는 작업이 필요하다. 쓰레드를 이용하는 방법은 프로그램의 수정을 최소화하면서 블로킹 I/O를 병렬로 수행하는 가장 간단한 방법이다.

     

     

     

     

    References

    파이썬 코딩의 기술 Effective Python, 똑똑하게 코딩하는 법

     

     

    반응형

    댓글