Back-End/Server

파이썬의 GIL 이란

.JStory 2022. 9. 5. 02:00
파이썬은 두 개 이상의 스레드가 바이트코드를 실행하는 것을 원하지 않는다. CPython의 메모리 관리 정책은 Thread Safe하지 않기 때문이다. 이러한 문제를 해결하고자 Python 바이트 코드를 실행하려면 인터프리터 잠금(GIL)을 획득해야 한다는 규칙을 추가했다.

 

GIL [Global Interpreter Lock]이란 Python Object 접근을 제한하는 mutex이다. GIL은 한 번에 하나의 스레드만이 파이썬 바이트코드를 실행하도록 보장하기 위해 CPython 인터프리터가 사용하는 메커니즘이다. PyObject를 묵시적으로 Thread Safe 하도록 만들어서 CPython의 구현부를 단순하게 만든다. 다시 말해 로직 그 자체에 집중한 CPython 구현부를 작성하며 그 외 변수들의 데드락과 같은 충돌을 고려하지 않을 수 있다는 것이다.

Mutex에 대한 설명은 링크로 남긴다.
[Microsoft] Mutex Docs

 

더군다나 CPython에서 메모리 관리를 위한 GC로 레퍼런스 카운팅 방식을 사용하는데, 멀티쓰레드가 동시에 하나의 자원을 참조하는 경우 레퍼런스 카운트가 2가 아닌 1이 증가하는 등 다중 스레드가 동시에 값을 늘리거나 줄이는 경쟁하게 되어 메모리 Leak이 발생할 수 있다.

이 문제를 해결하고자 각 Python Object 또는 Object 그룹에 Lock을 추가하면 Race Condition를 유발할 수 있고, 모든 변수에 Lock 만들어야 하므로 반복적인 잠금/해제로 인한 병목현상이 발생할 가능성이 높다.

 

GIL의 장/단점을 간단하게 정리해보았다.

 

장점

1. 교착상태 [데드락]을 방지한다.

2. 락이 하나뿐이므로 비교적 적은 성능 오버헤드를 발생시킨다.

단점

1. 모든 CPU 바인딩 Python 코드를 단일 스레드에서 돌아가기 때문에 다중 스레드로 얻을 수 있는 병렬성이 희생된다.

 

 그렇다면, 글로벌 락이 존재하는 파이썬에서 멀티스레딩은 불가능할까?

그렇지는 않다. 하지만 멀티스레딩의 성능 이점을 보기는 어렵다. CPython 내부적으로 하나의 스레드만이 자원에 접근할 권한을 갖기 때문이다. CPU 집약적인 명령을 실행하는 경우에는 스레드 컨텍스트 스위칭 오버헤드로 인해 오히려 전체적인 실행시간이 길어질 수 있다.

다만 동시성은 해결할 수 있다. Python의 multhreading은 동시성을 위해 쓰레드 스위치를 정기적으로 시도하기 때문이다.

아래는 테스트를 위한 간단한 샘플 코드이다.

import time
import concurrent.futures

MAX_NUM = 500000000  # 5억


def display_execution_time(func):

    def inner():
        start_time = time.time()
        value = func()
        end_time = time.time()
        print(f"실행시간: {round(end_time - start_time, 4)}s")
        return value
    return inner


def sum_range(start: int, end: int):
    result = 0
    print('sum start')
    for i in range(int(start), int(end)):
        result += i
    return result


@display_execution_time
def sync_sum():
    """순차적으로 MAX_NUM의 총 합을 계산한다"""
    return sum_range(1, MAX_NUM)


@display_execution_time
def multithreading_sum():
    """최대 100개의 스레드로 MAX_NUM을 나누어 총 합을 계산한다"""
    thread_num = 100
    with concurrent.futures.ThreadPoolExecutor(thread_num) as executor:
        start_num, end_num, step = 1, MAX_NUM // thread_num, MAX_NUM // thread_num
        threads = []

        while start_num < MAX_NUM:
            threads.append(executor.submit(sum_range, start_num, end_num))
            start_num += step
            end_num += step

        return sum([thread.result() for thread in threads])


if __name__ == '__main__':

    print(f"결과값: {sync_sum()}")

    print('-----------------------')

    print(f"결과값 [멀티스레딩]: {multithreading_sum()}")


# 결과
sum start
실행시간: 22.7735s
결과값: 124999999750000000
-----------------------멀티스레드
sum start
sum start
sum start
....
실행시간: 23.0175s
결과값: 124999975000000000

'sum start'가 병렬로 출력되는 것을 보면 병렬로 실행되는 것을 알 수 있다. 하지만 순차 계산과 비교하여 대체로 실행시간이 0.2초 가량 더 긴 것을 확인할 수 있다.

 

한편, CPU 연산이 아닌 I/O 대기가 발생하는 경우에는 Lock 이 해제되고 대기 중에 다른 연산을 진행하기 때문에 성능상의 이점을 볼 수 있다. 아래는 순차 / 멀티스레딩을 통해 파일 I/O를 발생시키는 간단한 테스트 코드이다.

import time
import threading
import json


def read_file():
    with open("./fixture.json", 'r') as f:
        json.load(f)


@display_execution_time
def sync_read_file():
    """순차적으로 500번의 JSON 파일 I/O가 발생한다"""
    for _ in range(500):
        read_file()


@display_execution_time
def multithreading_read_file():
    """500개의 스레드로 각 1번씩 JSON 파일 I/O가 발생한다"""
    threads = []

    for _ in range(500):
        t = threading.Thread(target=read_file)
        t.start()
        threads.append(t)

    for t in threads:
        t.join()


if __name__ == '__main__':

    sync_read_file()
    print('-----------------------멀티스레딩')
    multithreading_read_file()


# 결과
실행시간: 18.2038s
-----------------------멀티스레딩
실행시간: 16.5974s

위 결과처럼 I/O의 경우 18.15초 vs 16.97초 로 약 10%의 성능향상을 볼 수 있다.

 

 점점 서비스가 복잡해지고 H/W의 발전이 단일 코어의 성능향상보다는 많은 코어로 병렬성능이 개선되는 방향성을 보이고 있는 만큼, 병렬성이 중요해지고 있다. 이런 상황에서 단일 스레드로만 연산이 되는 파이썬의 GIL을 많은 개발자들이 상당히 아쉽게 여기는 것으로 보인다. 어찌보면 파이썬이라는 언어의 가장 큰 특징으로 손꼽히는 GIL은 많은 사람들이 극복하기 위해서 다양한 시도를 하고 있다. 

https://pyfound.blogspot.com/2022/05/the-2022-python-language-summit-python_11.html

 

The 2022 Python Language Summit: Python without the GIL

If you peruse the archives of language-summit blogs, you’ll find that one theme comes up again and again : the dream of Python without the...

pyfound.blogspot.com

Python 생태계의 수 많은 라이브러리들과의 호환성 등으로 인해 GIL을 극복하는 작업이 빠르게 진행되고 있는 것 같지는 않으나 언젠가 이 문제를 훌륭하게 해결한 방법을 만나보고 싶다.

 

 


https://docs.python.org/ko/3/glossary.html#term-global-interpreter-lock

https://wiki.python.org/moin/GlobalInterpreterLock

 

 

'Back-End > Server' 카테고리의 다른 글

HTTP 인증에 관한 개요  (0) 2022.05.08
무료 도메인(Free Domain) Freenom의 Not Available 문제.  (8) 2020.02.14