>  기사  >  백엔드 개발  >  Python GIL의 멀티스레딩 성능은 무엇입니까? GIL에 대한 심층 설명

Python GIL의 멀티스레딩 성능은 무엇입니까? GIL에 대한 심층 설명

PHPz
PHPz원래의
2017-04-23 17:08:031321검색

서문: 블로거는 Python을 처음 접할 때 GIL이라는 단어를 자주 듣게 되며, 이 단어는 멀티스레딩을 효율적으로 구현하는 Python의 무능력과 동일시되는 경우가 많습니다. 무슨 일이 일어나고 있는지 아는 것뿐만 아니라 왜 그런 일이 일어나고 있는지도 알고 있다는 연구 태도에 맞춰 블로거는 온갖 정보를 수집하고 일주일 동안 몇 시간의 자유 시간을 보내 GIL을 깊이 이해하고 이를 다음과 같이 요약했습니다. 또한 이 기사를 통해 독자들이 GIL을 더 잘, 객관적으로 이해할 수 있기를 바랍니다.

GIL이란 무엇인가요?

먼저 분명히 해야 할 것은 GIL은 Python의 기능이 아니라는 점입니다. 파서(CPython) 개념입니다. C++가 언어(문법) 표준 세트와 마찬가지로 다른 컴파일러를 사용하여 실행 가능한 코드로 컴파일될 수 있습니다. GCC, INTEL C++, Visual C++ 등과 같은 유명한 컴파일러. Python의 경우에도 마찬가지입니다. CPython, PyPy 및 Psyco와 같은 다양한 Python 실행 환경을 통해 동일한 코드가 실행될 수 있습니다. 예를 들어 JPython에는 GIL이 없습니다. 그러나 CPython은 대부분의 환경에서 기본 Python 실행 환경입니다. 따라서 많은 사람들의 개념에서는 CPython이 Python이고 GIL가 Python 언어의 결함에 기인한다고 당연하게 받아들입니다. 그러니 여기서 분명히 해두겠습니다: GIL은 Python의 기능이 아닙니다. Python은 GIL에 전혀 의존할 필요가 없습니다.

그러면 CPython 구현에서 GIL은 무엇입니까? GIL의 전체 이름 Global Interpreter Lock 오해를 피하기 위해 공식 설명을 살펴보겠습니다.

CPython에서 전역 해석기 잠금(GIL)은 다중 네이티브를 방지하는 뮤텍스입니다. 이 잠금은 주로 CPython의 메모리 관리가 스레드로부터 안전하지 않기 때문에 필요합니다(그러나 GIL이 존재하기 때문에 다른 기능은 적용되는 보장에 따라 달라졌습니다.)

그럼 안 좋아 보이지 않나요? 여러 스레드가 기계어 코드를 동시에 실행하는 것을 방지하는 뮤텍스는 얼핏 보면 버그처럼 보이는 전역 잠금처럼 보입니다. 걱정하지 마세요. 아래에서 천천히 분석해 보겠습니다.

GIL이 왜 있는 걸까요

물리적 한계로 인해 CPU 제조사 간 코어 주파수 경쟁이 멀티코어로 대체되었습니다. 멀티코어 프로세서의 성능을 보다 효과적으로 활용하기 위해 멀티스레드 프로그래밍 방식이 등장했는데, 이로 인해 스레드 간의 데이터 일관성 및 상태 동기화가 어려워졌습니다. CPU 내부의 캐시도 예외는 아니며 여러 캐시 간의 데이터 동기화를 효과적으로 해결하기 위해 다양한 제조업체에서 많은 노력을 기울였으며 이는 필연적으로 특정 성능을 가져옵니다. 손실.

물론 파이썬은 탈출할 수 없습니다. 멀티 코어를 활용하기 위해 파이썬은 멀티스레딩을 지원하기 시작했습니다. 여러 스레드 간의 데이터 무결성 및 상태 동기화를 해결하는 가장 간단한 방법은 자연스럽게 잠그는 것입니다. 따라서 GIL에는 매우 큰 잠금 장치가 있으며, 점점 더 많은 코드 기반 개발자가 이 설정을 수락하면 이 기능에 크게 의존하기 시작합니다(예: 기본 Python 내부 객체 스레드로부터 안전합니다. 구현 중에 추가 메모리 잠금 및 동기화 작업을 고려할 필요가 없습니다.

이 구현 방법은 점차 고통스럽고 비효율적이라는 것이 밝혀졌습니다. 그러나 모두가 GIL을 분리하고 제거하려고 시도했을 때 많은 수의 라이브러리 코드 개발자가 GIL에 크게 의존해 왔으며 이를 제거하는 것이 매우 어렵다는 것을 알게 되었습니다. 얼마나 어려운가요? 비유하자면, MySQL과 같은 "작은 프로젝트"는 Buffer Pool Mutex의 큰 잠금을 다양한 작은 잠금으로 분할하는 데 5.5년에서 5.6년, 5.7년 정도가 거의 5년이 걸렸으며 여전히 진행되고 있습니다. 회사 지원과 그 뒤에 고정된 개발 팀이 있는 제품인 MySQL은 Python과 같은 핵심 개발자 및 코드 기여자로 구성된 고도로 커뮤니티 기반 팀은 말할 것도 없고 그렇게 어려운 시간을 보내고 있습니까?

간단히 말하면 GIL의 존재는 역사적인 이유에 더 가깝습니다. 이 작업을 다시 수행해야 한다면 여전히 멀티스레딩 문제에 직면해야 하지만 적어도 현재 GIL 접근 방식보다 더 우아할 것입니다.

GIL의 영향

위의 소개와 공식 정의로 볼 때 GIL은 의심할 여지 없이 글로벌 독점 잠금 장치입니다. 글로벌 잠금의 존재가 멀티스레딩의 효율성에 큰 영향을 미칠 것이라는 점에는 의심의 여지가 없습니다. 마치 Python이 단일 스레드 프로그램인 것과 거의 같습니다. 그러면 독자들은 전역 잠금이 해제되는 한 효율성이 나쁘지 않을 것이라고 말할 것입니다. 시간이 많이 걸리는 IO 작업을 수행할 때 GIL을 해제할 수 있는 한 운영 효율성은 여전히 ​​향상될 수 있습니다. 즉, 아무리 나빠도 싱글 스레드의 효율성보다 나쁘지는 않습니다. 이론상으로는 맞는데 실제로는 그럴까요? 파이썬은 당신이 생각하는 것보다 더 나쁩니다.

멀티스레딩과 싱글스레딩에서 파이썬의 효율성을 비교해 보겠습니다. 테스트 방법은 매우 간단합니다. 1억 번 반복하는 카운터 함수입니다. 하나는 단일 스레드를 통해 두 번 실행되고, 다른 하나는 여러 스레드를 통해 실행됩니다. 마지막으로 총 실행 시간을 비교합니다. 테스트 환경은 듀얼 코어 Mac Pro입니다. 참고: 스레드 라이브러리 자체의 성능 손실이 테스트 결과에 미치는 영향을 줄이기 위해 여기의 단일 스레드 코드에서도 스레드를 사용합니다. 단일 스레드를 시뮬레이션하려면 순차적으로 두 번 실행하면 됩니다.

단일 스레드를 순차적으로 실행(single_thread.py)

#! /usr/bin/pythonfrom threading import Threadimport timedef my_counter():    i = 0    for _ in range(100000000):        i = i + 1    return Truedef main():    thread_array = {}    start_time = time.time()    for tid in range(2):        t = Thread(target=my_counter)        t.start()        t.join()    end_time = time.time()    print("Total time: {}".format(end_time - start_time))if name == 'main':    main()

두 개의 동시 스레드를 동시에 실행(multi_thread.py)

#! /usr/bin/pythonfrom threading import Threadimport timedef my_counter():    i = 0    for _ in range(100000000):        i = i + 1    return Truedef main():    thread_array = {}    start_time = time.time()    for tid in range(2):        t = Thread(target=my_counter)        t.start()        thread_array[tid] = t    for i in range(2):        thread_array[i].join()    end_time = time.time()    print("Total time: {}".format(end_time - start_time))if name == 'main':    main()

아래와 같이 테스트 결과는

Python GIL의 멀티스레딩 성능은 무엇입니까? GIL에 대한 심층 설명

입니다. 실제로 멀티 스레드의 경우 Python이 단일 스레드보다 45% 느린 것을 볼 수 있습니다. 이전 분석에 따르면 GIL 전역 잠금이 존재하더라도 직렬화된 멀티스레딩은 단일 스레딩과 동일한 효율성을 가져야 합니다. 그렇다면 어떻게 그렇게 나쁜 결과가 나올 수 있었을까요?

GIL의 구현 원리를 통해 그 이유를 분석해 보겠습니다.

현재 GIL 설계의 결함

pcode 수에 따른 스케줄링 방법

Python 커뮤니티의 아이디어에 따르면 스레드 스케줄링은 운영 체제 자체는 이미 매우 성숙해 있으므로 일단 안정적이면 직접 만들 필요가 없습니다. 따라서 Python 스레드는

C 언어의 pthread이며 운영 체제 스케줄링 알고리즘을 통해 예약됩니다(예: linux는 CFS입니다). 각 스레드가 CPU 시간을 균등하게 활용할 수 있도록 Python은 현재 실행되는 마이크로코드 수를 계산하고 특정 임계값에 도달하면 GIL을 강제로 해제합니다. 이때 운영체제의 스레드 스케줄링도 함께 작동하게 된다(물론 실제로 컨텍스트 전환이 수행되는지 여부는 운영체제에 따라 결정된다).

의사 코드

while True:    acquire GIL    for i in 1000:        do something    release GIL    /* Give Operating System a chance to do thread scheduling */
이 모드는 CPU 코어가 하나만 있는 경우에는 문제가 없습니다. 모든 스레드는 깨어나면 성공적으로 GIL을 얻을 수 있습니다(스레드 스케줄링은 GIL이 해제될 때만 발생하기 때문입니다). 그러나 CPU에 코어가 여러 개 있으면 문제가 발생합니다. 의사코드에서 볼 수 있듯이

release GIL 사이에는 간격이 거의 없습니다. 따라서 다른 코어의 다른 스레드가 깨어나면 대부분의 경우 기본 스레드가 GIL을 다시 획득합니다. 이때 실행을 위해 깨어난 스레드는 다른 스레드가 GIL을 사용하여 성공적으로 실행되는 것을 지켜보며 CPU 시간만 낭비할 수 있습니다. 그러다가 전환 시간에 도달한 후 대기 상태로 진입했다가 다시 깨어나고 다시 대기하는 악순환을 반복하게 된다. acquire GIL

PS: 물론 이 구현은 원시적이고 보기 흉합니다. GIL과 스레드 스케줄링 간의 상호 작용은 각 Python 버전에서 점차 개선됩니다. 예를 들어 먼저 스레드 컨텍스트 전환을 수행하는 동안 GIL을 유지하고, IO를 기다리는 동안 GIL을 해제하십시오. 그러나 변경할 수 없는 것은 GIL의 존재로 인해 이미 비용이 많이 드는 운영 체제 스레드 스케줄링 작업이 더욱 고급스러워진다는 것입니다. GIL의 영향에 대한 확장 읽기

GIL이 멀티스레딩에 미치는 성능 영향을 직관적으로 이해하기 위해 직접 차용한 테스트 결과 차트를 소개합니다(아래 그림 참조). 그림은 듀얼 코어 CPU에서 두 스레드의 실행을 보여줍니다. 두 스레드 모두 CPU 집약적인 컴퓨팅 스레드입니다. 녹색 부분은 스레드가 실행 중이고 유용한 계산을 수행하고 있음을 나타냅니다. 빨간색 부분은 스레드가 깨어나도록 예약되었지만 GIL을 얻을 수 없어 효과적인 계산을 수행할 수 없음을 나타냅니다.

그림에서 볼 수 있듯이 GIL의 존재로 인해 멀티 스레딩이 멀티 코어 CPU의 GIL Performance동시 처리 기능을 완전히 활용할 수 없게 됩니다.

그렇다면 Python의 IO 집약적 스레드가 멀티스레딩의 이점을 누릴 수 있습니까? 아래에서 테스트 결과를 살펴보겠습니다. 색상의 의미는 위의 그림과 같습니다. 흰색 부분은 IO 스레드가 대기 중임을 나타냅니다. IO 스레드가 데이터 패킷을 수신하고 터미널을 전환하게 할 때 CPU 집약적인 스레드의 존재로 인해 여전히 GIL 잠금을 얻을 수 없어 끝없는 대기 루프가 발생하는 것을 볼 수 있습니다.

GIL IO Performance

간단히 요약하면 다음과 같습니다. 멀티 코어 CPU의 Python 멀티스레딩은 CPU 집약적인 스레드가 하나 이상 있는 경우에만 IO 집약적인 계산에 긍정적인 영향을 미칩니다. GIL로 인해 크게 떨어질 것입니다.

GIL의 영향을 받지 않는 방법

말을 너무 많이 해서 해결책을 언급하지 않으면 그냥 대중적인 과학 포스팅일 뿐이지만, 쓸모 없는. GIL이 너무 나빠요. 해결할 수 있는 방법이 있나요? 어떤 솔루션이 있는지 살펴보겠습니다.

스레드를 멀티프로세싱으로 대체

멀티프로세싱 라이브러리의 등장은 GIL로 인한 스레드 라이브러리의 비효율성을 보완하기 위한 것이 크다. 마이그레이션을 용이하게 하기 위해 스레드에서 제공하는 인터페이스 세트를 완전히 복제합니다. 유일한 차이점은 다중 스레드 대신 다중 프로세스를 사용한다는 것입니다. 각 프로세스에는 독립적인 GIL이 있으므로 프로세스 간에 GIL 경합이 발생하지 않습니다.

물론 멀티프로세싱이 만병통치약은 아닙니다. 이 기능을 도입하면 프로그램의 시간 스레드 간 데이터 통신 및 동기화가 어려워집니다. 카운터를 예로 들어보겠습니다. 여러 스레드가 동일한 변수 를 누적하도록 하려면 스레드의 경우 전역 변수를 선언하고 thread.Lock 컨텍스트로 세 줄을 래핑합니다. 다중 처리에서는 프로세스가 서로의 데이터를 볼 수 없기 때문에 메인 스레드에서 대기열을 선언하고, 넣은 다음 가져오거나, 공유 메모리를 사용할 수만 있습니다. 이러한 추가 구현 비용은 이미 매우 고통스러운 멀티 스레드 프로그램 코딩을 더욱 고통스럽게 만듭니다. 구체적인 어려움은 무엇입니까? 관심 있는 독자는 이 기사를 더 읽어볼 수 있습니다

다른 파서 사용

앞서 언급했듯이 GIL은 CPython의 제품이므로 다른 파서가 더 좋습니까? 예, JPython 및 IronPython과 같은 파서는 구현 언어의 특성으로 인해 GIL의 도움이 필요하지 않습니다. 그러나 파서 구현을 위해 Java/C#을 사용함으로써 커뮤니티 C 언어 모듈의 많은 유용한 기능을 활용할 기회도 잃었습니다. 따라서 이러한 파서는 항상 상대적으로 틈새 시장이었습니다. 결국 다들 초기에는 기능보다는 전자를 선택하게 되겠죠, Done is better than perfect.

그래서 절망적이라는 겁니까?

물론 Python 커뮤니티에서도 GIL을 지속적으로 개선하기 위해 열심히 노력하고 있으며 심지어 GIL을 제거하려는 시도도 하고 있습니다. 그리고 각 마이너 버전마다 많은 개선이 이루어졌습니다. 관심 있는 독자는 이 슬라이드를 자세히 읽을 수 있습니다. 또 다른 개선 사항 GIL 재작업 - opcode 계산에서 타임 슬라이스 계산으로 전환 세분성 변경 - 최근에 GIL 잠금을 해제한 스레드가 즉시 다시 예약되는 것을 방지 - 새로 추가됨스레드우선순위기능(우선순위가 높은 스레드는 다른 스레드가 자신이 보유한 GIL 잠금을 해제하도록 강제할 수 있음)

요약

Python GIL은 실제로 기능과 성능의 조합입니다. 그것은 시간과 공간의 절충, 특히 존재의 합리성의 산물이며, 바꾸기 어려운 객관적인 요소도 갖고 있다. 이 부분의 분석을 통해 다음과 같은 간단한 결론을 내릴 수 있습니다. - GIL이 있기 때문에 IO Bound 시나리오에서는 다중 스레드만이 더 나은 성능을 얻을 수 있습니다. - 높은 병렬 컴퓨팅 성능으로 프로그래밍하려는 경우 다음을 고려할 수 있습니다. 핵심을 사용하여 일부 부분은 C 모듈로 변환되거나 단순히 다른 언어로 구현됩니다. - GIL은 오랫동안 계속 존재하지만 계속 개선됩니다

참조

Python의 가장 어려운 문제 GIL에 관한 공식 문서 스레드 우선순위와 새로운 GIL 재검토


위 내용은 Python GIL의 멀티스레딩 성능은 무엇입니까? GIL에 대한 심층 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.