20130813

python old gil and new gil

출처 : http://methane.hatenablog.jp/entry/20111203/1322900647
참고 : http://www.dabeaz.com/GIL/gilvis/index.html


Python 3 이 릴리스 되고난후 Python 의 진화는 주로 Python 3 에서 일어나 Python 2 에 백포트 할수 있는것이 백포드 되어 있다.
(예: GC의 튜닝, 사전의 내장표기등)

하지만 Python 2 는 2.7에서 신규개발을 종료해 2.7에 백포트되지 않았던 기능은 Python 3에서 사용할수 있다.
오늘은 그런 기능중에 하나인 New GIL에 대해서 소개하겠다.


소개할 내용은 거의 UnderstaindingGIL 에서 소개되고 있는 내용을 내나름대로 요약한것이다.(단순 번역이 아님)


GIL란
CPython의 스레드는 OS의 스레드와 1대1 대응으로(네이티브 스레드), 예를 들면 복수의 시스템 콜을 병렬로 실행할수 있거나 bz2압축처럼 C언어 로 적혀진 무거운 처리를 병렬로 실행할수 있다.

하지만 Python의 코드를 실행하거나 Python의 오브젝트를 조작할수 있는 스레드는 항상 한개이다. 이것은 싱글 스레드시의 성능이나 Python의 구현을 심플하게 하기 위한 설계이다.

이렇게 한개의 스레드밖에 실행할수 없도록 하기 위해 존재하는것이 GIL = Global Interpreter Lock이다. 약자를 풀어서 읽어보면 인터프리터전체에 걸리는 락을 의미한다. 이 락을 취득하고 있는 스레드만이 Python의 코드를 실행하거나 Python의 오브젝트를 조작할수 있다.


Old GIL
Python 2.x나 Python 3.1 까지의 GIL을 Old GIL라고 부른다. Old GIL의 동작은 매우 단순하다.

블럭하는 시스템 콜을 실행하는 경우에서는 GIL을 개방하고나서 실행하고 완료하면 GIL을 재취득한다. (10페이지)
Python 의 코드를 계속실행하는 경우는 checkinterval에서 정해진 기간안에서 실행하고 그후 GIL을 개방한후 재취득한다.(11-14페이지)
Old GIL의 문제점을 설명하기 전에 GIL의 구현에 관해 조금 설명하겠다. (18페이지) 플랫폼마다 세세한 부분에서 구현이 틀리지만 대충이야기하면 GIL는 락변수와 통지기구로 구성되어 있다. 락변수는 1에서 0로 바꾸는 디크리멘트, 0에서 1로 바꾸는 인크리멘트를 atomic으로 수행할수 있는것이라 하자

GIL을 취득하는 경우는 락변수의 디크리멘트를 수행, 실패하면 (락 변수의 값이 0이 였다) 통지를 기다리며 슬립, 통지가 오면 다시 락변수의 디크리멘트를 수행하는 동작을 하여 디크리멘트가 가능할때까지 반복한다.

GIL을  개방하는 경우는 락변수를 인크리멘트한다. 이때는 자신이 GIL을 가지고 있으므로 인크리멘트는 확실하게 성공할것이다. 인크리멘트한후에 통지를 기다리고 있는 한개의 스레드에게 통지를 보낸다. (어짜피 GIL을 취득할수 있는 스레드는 1개이므로 복수의 스레드에 통지를 보내도 의미가 없기때문이다.)

이 기구에 의해 어떻게 스레드의 실행권이 바뀌는지 생각할수 있다.


먼저 GIL을 가지고 있는 슬[드가 블럭할 시스템 콜을 실행하기전에 GIL을 개방했다고 하자 이 경우는 통지를 받고 슬립 상태에서 일어난 스레드가 성공적으로 GIL을 취득한다. 외에 누구도 GIL의 취득하려 시도하지 않고 있기 때문이다.(19-21페이지)

다음에 checkinterval가 경과해 GIL을 개방한후 바로 재취득하는 경우를 관해 생각해 보자 이 경우 GIL의 취득을 시도하는 스레드는 GIL을 개방한 스레드와 GIL을 서로 빼앗게 된다. (22페이지)

싱글 코어의 경우는 동시에 실행할수 있는 스레드가 1개 뿐이므로 락을 개방해서 통시를 보낼때에 통지를 보낸곳과 통지를 받는곳 언느쪽의 스레드가 실행될지는 OS가 판단한다.(24-26페이지) OS의 스케쥴러가 현명하다면 지금까지 GIL을 취드하고 있던 스레드(checkinterval을 경과해 개방후 재 취득하려던 스레드)가 짧은 시간밖에 실행되지 못했하면 컨텍스트 스위치의 회수가 늘어나지 않도록 그대로 실행하도록 할것이고 이미 장시간 실행했다면 바로 통지를 받는 스레드(슬립상태의 스레드)로 스위치할것이다. 이렇게 해서 OS에 우선된 스레드가 GIL을 취득하게 된다.(32페이지)


하지만 시대는 멀티스레드이다. 멀티코어에서는 동시에 실행할수 있는 스레드가 2개 이상이므로 락을 개방해 통지를 보낸 스레드는 그대로 실행을 해 병렬해서 통지를 받은 스레드가 실행을 재개한다. 스레드가 통지를 받고 실행을 재개하는데는 타임랙이 있으므로 대개는 GIL을 개방한 쪽의 스레드가 그대로 GIL을 확보해버린다.(33페이지) 장시간Python코드를 실행한 스레드가 있다면 그 밖의 스레드는 상당히 GIL을 취득하기가 어려워진다.(34페이지)



그외에도 몇몇 문제가 있다.

파일로부터 읽기를 수행시 이미 파일이 OS에 캐쉬되어 있는 경우등 블럭「할지도 모르는」시스템 콜이 곧 돌아오는 일도 자주 있다.
이 때 시스템 콜의 전후에서 GIL의 개방과 취득을 하고 있으므로 대량으로 통지가 보내져 불필요한 부하가 증가하는 경우도 있다.(35페이지)

checkinterva가 시간이 하니고 바이트 코드단위로 기간을 지정하고 있으므로 체크 간격이 너무 짧아서 오버해드가 커지거나 반대로 너무 길어서 레이턴시가 나빠지거나 할 가능성도 있다.

싱글 코어에서 CPU 바운드 스레드가 많이 있는경우는 OS의 컨텍스트 스위치를 증가시켜 스루풋이 저하할 가능성도 있다.


New GIL
Python 3.2부터 새로운GIL가 탑재되었다. New GIL에서는 Old GIL의 checkinterval 관련 동작이 전혀 달라졌다.
checkinterval는 없어지고 이를 대신해 타임아웃 시간을 지정하는 switchinterval이 도입되었다. (sys.getswitchinterval(), sys.setswitchinterval() 으로 참조와 변경이 가능. 기본값은 5ms)

New GIL의 기본적인 락 기구는 Old GIL과 동일하지만 장시간 Python 코드를 실행하는 스레드로 부터 GIL를 빼앗는 구조가 다르다.

checkinterval 마다 GIL을 개방, 재취득하는것이 아니고 gil_drop_request 라는 플래그가 있으면 강제적으로 GIL을 개방하는 것이 된다.(39페이지)

GIL을 취득하고 싶은 스레드는 타임아웃을 지정해 Old GIL과 동일한 방법으로 락의 취득을 기다린다.(42페이지) 타임아웃할때 까지 지금까지 실행하고 있던 스레드가 I/O처리등으로 GIL을 개방해 슬립한 경우는 여느때처럼 GIL을 취득한다.(43 페이지)

타임아웃이 발생한 경우 그 스레드는 gil_drop_request을 설정하고 더 기다린다. (44페이지) 이 플래그를 본 실행중인 스레드가 GIL을 개방하는것인데 Old GIL과 달리 바로 GIL을 재취득하지 않는다. 이로 인해 멀티코어에서도 정확히 GIL이 이양된다.(45페이지)
그 대신 새롭게 GIL을 취득한 스레드가 GIL을 취득한 것을 GIL를 개방한 스레드에세 통지한다. GIL을 개방한 스레드는 그 통지를 받고나서 GIL의 재취득 대기에 들어 간다. (46-47페이지)

또한 이 타임아웃은 바이트 코드 숫자가 아닌 시간으로 바뀌었기 때문에 바이트 코드 해당하는 실행시간이 따로따로 지정되 전환가격이 너무 길거나 너무 짧거나 하지는 않는다.

CPU 바운드 스레드가 많이 있는 경우도 이 타임아웃 시간안에는 1개의 스레드가 점유 할수 있으므로 스루풋의 저하가 쉽게 일어나지 않게 된다.


New GIL의 결점
아쉽지만 이것으로 모든것이 해결이라고 할수 없다. 오히려 New GIL이 Old GIL에 뒤처지는 장면되 있다.
그 전형적인 예가 CPU 바운드 스레드와 IO 바운드 스레드의 조합이다.(50페이지)

IO 처리가 완료해서 GIL을 취득하려고 할때 타임아웃 시간내에는 GIL을 취득할수 없으므로 레이턴시가 나빠진다.(51페이지)
바로 돌아오는 IO처리를 반복하는 경우는 이 타임아웃 시간을 거듭하므로 대폭적인 성능저하로 이어진다.(53페이지)

그 외에도 실행대기의 스레드가 복수로 있는 경우에 먼저 기다리고 있던 스레드에 실행권이 넘겨지지 않는 문제도 있다. 이것은 타임아웃 지정의 시그널 대기를 반복할때 시그널대기의 큐의 맨마지막으로 돌려져버리기 때문에 이 스레드보다 뒤에 대기하고 있던 스레드가 GIL취득 순서가 앞서게 되어버리기 때문이다. (52페이지)

이런 결점을 개선하기 위해 현재 스레드에 우선순위를 부여하자는 제안이나 보다 정중하게 블럭하지 않는 IO를 판별해 GIL 개방을 하지않도록 하자는등 제안되고 있다. Python 3.3에서 개선되어 나왔으면 한다.


마지막으로
Old GIL과 New GIL의 간단한 설명과 결점을 소개했다. 특히나 큰문제는 CPU 바운드 스레드 처리를 하면서 다른 스레드도 시행하고 있는 케이스에서 발생하므로 멀티스레드 Python 프로그래밍이 CPU를 100% 이용하고 있는 경우는 CPU 바운드 처리를 multiprocessing.Process 등을 이용해 별도의 프로세스로 나뉘어 처리하는편이 좋을것이다. 응답성능의 문제를 회필할수 있는만이 아닌 멀티 코어를 이용해서 병렬계산을 할수있게 된다.



댓글 없음:

Articles