python Interpreter
GIL 이해를 위해 수행되어야 하는 것은 파이썬 인터프리터의 이해입니다.
- 파이썬 인터프리터 : 파이썬 문법으로 작성된 코드를 한 줄씩 읽으면서 실행하는 프로그램
- 파이썬 인터프리터의 표준은 CPython입니다.
- C언어를 이용하여 구현한 파이썬 인터프리터입니다.
GIL(Goloba Interpreter Lock)
- 전역 인터프리터 락.
- 위키에서의 정의는 아래와 같음.
> In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once. This lock is necessary mainly because CPython's memory management is not thread-safe.
요약하면
- 뮤택스임
- 파이썬 객체에 접근을 보호하기 위한 것
- 여러개의 쓰레드가 파이썬 코드를 동시에 실행하지 못하도록 하는 것
- 한 프로세스 내에서, 파이썬 인터프리터는 한 시점에 하나의 쓰레드에의해서만 실행 될 수 있음.
- 멀티 쓰레딩이 불가능한건 아님. -> 멀티 코어라면 멀티 쓰레딩 시에 여러 개의 쓰레드가 여러 코어 상에서 병렬 실행을 할수 있음
하지만 파이썬에서는 그런 병렬 실행이 불가능하다는것뿐
그럼 왜? GIL 필요한 거임?
- 뮤택스로써의 역할을 하기 때문에 그렇다고 이미 앞서 언급되었다.
- Mutual Exculsion Object는 상호 배제를 의미합니다.
- 더 설명하면, 파이썬의 모든 것은 객체입니다. 각 객체는 참조횟수(Reference Count)를 저장하기 위한 필드를 갖고 있습니다.
참조 횟수란 그 객체를 가리키는 참조가 몇개 존재하는지를 나타내는 것으로, Python에서의 GC는 이러한 참조 횟수가 0이 되면
해당 객체를 메모리에서 삭제시키는 메커니즘으로 동작하고 있습니다.sys
라이브러리의getrefcount()
함수를 사용하면 특정
객체의 참조 횟수를 알아 낼 수 있습니다.무슨 말 일까? 참조 횟수에 기반하여 GC를 진행하는 Python의 특성상, 여러 개의 쓰레드가 Python인터프리터를 동시에 실행하면 Race Condition이
발생 할 수 있기 때문입니다. Race Condition이란, 하나의 값에 여러 쓰레드가 동시에 접근함으로써 값이 올바르지 않게 읽히거나 쓰일 수 있는 상태입니다. 이러한 상황을 보고 Tread-safe하지 않다고 표현합니다. import sys x = [] # x참조 횟수 1 y = x # x의 참조 횟수 2 # getrefcount() 함수의 매개변수 할당 시 x의 참조 횟수가 1증가(3이됨) # getrefcount() 함수의 반환 시 x의 참조 횟수가 다시 1감소 (2가됨) sys.getrefcount(x) # 출력결과 : 3
즉, 여러 쓰레드가 파이썬 인터프리터를 동시에 실행할 수 있게 되면 각 객체의 참조 횟수가 올바르게 관리되지 못할수도 있고, 이로 인해 GC가 제대로 동작하지 않을 수 있습니다. 물론 이러한 Race Condition은 Mutext를 이용하면 예방할 수 있습니다.
뮤텍스(Mutex)란, 멀티 쓰레딩 환경에서 여러 개의 쓰레드가 어떠한 공유 자원에 접근 가능할 때 그 공유 자원에 접근하기 위해 가지고 있어야 하는 일종의 열쇠와 같은 것이다. 만약 한 쓰레드가 어떠한 공유 자원에 대한 뮤텍스를 가지고 있다면, 다른 쓰레드들은 그 공유 자원에 접근하고 싶을 때도 그 공유 자원에 접근하고 있는 쓰레드가 뮤텍스를 풀어줄 때까지는 기다려야 한다. 다음 그림은 뮤텍스의 개념을 비유적으로 표현한 그림이다.
그런데 앞서 말했듯이, Python에서 모든 것은 객체이고, 객체는 모두 참조 횟수를 가진다. 따라서 GC의 올바른 동작을 보장하려면 결국 모든 객체에 대해 뮤텍스를 걸어줘야 한다는 말이 된다. 이는 굉장히 비효율적이며, 만약 이를 프로그래머에게 맡길 경우 상당히 많은 실수를 유발할 수도 있는 문제이다.
그래서 결국 Python은 마음 편한 전략을 택하였다. 애초에 한 쓰레드가 Python 인터프리터를 실행하고 있을 때는 다른 쓰레드들이 Python 인터프리터를 실행하지 못하도록 막는 것이다. 이를 보고 "인터프리터를 잠갔다"라고 표현한다. 즉, Python 코드를 한 줄씩 읽어서 실행하는 행위가 동시에 일어날 수 없게 하는 것이다. 그러면 모든 객체의 참조 횟수에 대한 Race Condition을 고민할 필요도 없어진다. 뮤텍스를 일일이 걸어줄 필요도 없어지는 것이다. 이것의 GIL의 존재 이유이다.
파이썬 멀티쓰레딩은 언제 사용하는가?
위에서 설명한 것만 보면, Python에서는 GIL 때문에 멀티 쓰레딩을 쓰지 않는 게 좋아 보인다. 실제로, CPU 연산의 비중이 큰 작업을 할 때 멀티 쓰레딩은 오히려 성능을 떨어뜨린다. 병렬적인 실행은 불가능한데 괜히 문맥 전환(Context Switching) 비용만 잡아먹기 때문이다. 다음 예시 코드를 보자. 멀티 쓰레딩을 사용하니 오히려 더 느려진 걸 볼 수 있다.
import time
import threading
def loop():
for i in range(50000000):
pass
# Single Thread
start = time.time()
loop()
loop()
end = time.time()
print('[Single Thread] total time : {}'.format(end - start))
# Multi Thread
start = time.time()
thread1 = threading.Thread(target=loop)
thread2 = threading.Thread(target=loop)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
end = time.time()
print('[Multi Thread] total time : {}'.format(end - start))
# [Single Thread] total time : 2.3374178409576416
# [Multi Thread] total time : 3.4128201007843018
하지만 GIL은 CPU의 연산 과정에서 공유 자원에 대해 발생할 수 있는 Race Condition 문제 때문에 필요했다는 걸 상기할 필요가 있다. 따라서 Python에서는 외부 연산(I/O, Sleep 등)을 하느라 CPU가 아무것도 하지 않고 기다리기만 할 때는 다른 쓰레드로의 문맥 전환을 시도하게 되어 있다. 이때는 다른 쓰레드가 실행되어도 공유 자원의 Race Condition 문제가 발생하지 않기 때문이다.
이러한 이유로, CPU 연산의 비중이 적은, 즉 외부 연산(I/O, Sleep 등)의 비중이 큰 작업을 할 때는 멀티 쓰레딩이 굉장히 좋은 성능을 보인다. 따라서 Python에서 멀티 쓰레딩이 무조건 안 좋다는 말은 사실이 아니다. 실제로, I/O 혹은 Sleep 등의 외부 연산이 대부분이라면 멀티 쓰레딩을 통해 큰 성능 향상을 기대할 수 있다. 다음 예시 코드를 보자. 멀티 쓰레딩을 통해 더 빨라진 걸 볼 수 있다.
import time
import threading
def sleep_for_2s():
time.sleep(2)
# Single Thread
start = time.time()
sleep_for_2s()
sleep_for_2s()
end = time.time()
print('[Single Thread] total time : {}'.format(end - start))
# Multi Thread
start = time.time()
thread1 = threading.Thread(target=sleep_for_2s)
thread2 = threading.Thread(target=sleep_for_2s)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
end = time.time()
print('[Multi Thread] total time : {}'.format(end - start))
# [Single Thread] total time : 4.017191171646118
# [Multi Thread] total time : 2.002999782562256
용어 정의
Mutex(뮤텍스, Mutual Exclusion Object)는 상호 배제를 의미합니다. 이는 다중 스레드 프로그래밍에서 공유 자원에 대한 동시 접근을 제어하는 메커니즘으로 사용됩니다. 즉, 한 시점에 하나의 스레드만 특정 자원을 사용할 수 있도록 하는 잠금(Lock)입니다.
뮤텍스는 데이터의 일관성과 무결성을 보장하기 위해 필요합니다. 여러 스레드가 동시에 같은 데이터에 접근하려 할 때, 데이터가 예기치 않게 변경되거나 손상될 수 있는데, 뮤텍스를 사용함으로써 한 번에 한 스레드만이 데이터나 자원에 접근할 수 있게 되어 이러한 문제를 방지할 수 있습니다.
작동 방식:
- 잠금(Lock) 획득: 스레드가 공유 자원을 사용하고자 할 때, 먼저 뮤텍스를 획득(잠금)합니다. 이 시점에서 해당 자원은 해당 스레드에 의해 "잠겨" 있으며 다른 스레드는 접근할 수 없습니다.
- 자원 사용: 잠금을 획득한 스레드는 필요한 작업을 수행합니다.
- 잠금 해제(Unlock): 작업이 완료되면 스레드는 뮤텍스를 해제(잠금 해제)하여 다른 스레드가 자원에 접근할 수 있도록 합니다.
뮤텍스의 중요성:
- 데이터 무결성: 동시에 같은 자원을 수정하려는 여러 스레드로 인해 발생할 수 있는 데이터 무결성 문제를 방지합니다.
- 동시성 제어: 프로그램의 동시성을 관리하고 제어하는 데 필요한 중요한 도구입니다. 적절한 동기화 없이 동시에 실행되는 스레드는 예측할 수 없는 결과를 초래할 수 있습니다.
주의점:
뮤텍스 사용 시 데드락(deadlock)과 같은 문제에 주의해야 합니다. 데드락은 두 개 이상의 스레드가 서로가 잠금을 해제하기를 기다리면서 영원히 대기 상태에 빠지는 현상을 말합니다. 따라서, 뮤텍스를 사용할 때는 잠금과 잠금 해제를 적절하게 관리하여 이러한 문제를 방지해야 합니다.
질문
CPI, I/O바운드 작업을 어떻게 명확히 구분 할 수 있어?
CPU 바운드 작업과 I/O 바운드 작업을 구분하는 기준은 작업을 수행하는 동안 시스템의 자원 사용 패턴에 있습니다. 여기서 '바운드(bound)'란 해당 작업의 성능이 주로 어떤 자원의 가용성에 의해 제한되는지를 나타냅니다.
CPU 바운드 작업:
CPU 바운드 작업은 프로세서의 계산 능력에 의해 성능이 제한되는 작업입니다. 즉, 작업을 완료하는 데 가장 큰 병목 현상이 CPU의 처리 속도에 있다는 것을 의미합니다. 이러한 작업은 주로 복잡한 수학적 계산, 데이터 처리, 암호화 연산 등을 포함합니다.
- 구분 방법:
- 작업 수행 시 CPU 사용률이 매우 높아집니다.
- 디스크 I/O나 네트워크 대기 시간보다, CPU에서 수행하는 계산이 작업 시간의 대부분을 차지합니다.
- 프로세서의 속도를 높이거나 코드를 최적화하면 작업 성능이 크게 향상됩니다.
I/O 바운드 작업:
I/O 바운드 작업은 입력/출력 연산에 의해 성능이 제한되는 작업입니다. 이는 시스템의 CPU가 아닌, 디스크 접근, 네트워크 통신, 사용자 입력 등의 I/O 작업에 더 많은 시간을 소비한다는 것을 의미합니다.
- 구분 방법:
- 작업 수행 시 디스크 사용률이나 네트워크 사용률이 높아지지만, CPU 사용률은 상대적으로 낮게 유지됩니다.
- 대기 시간이 주로 외부 데이터의 접근이나 전송에서 발생합니다.
- I/O 성능(예: SSD 사용, 네트워크 대역폭 향상)을 개선하면 작업 성능이 향상됩니다.
실제 구분 예:
- CPU 바운드 예: 대규모 배열의 정렬, 고해상도 그래픽 처리, 복잡한 수치 계산
- I/O 바운드 예: 웹 사이트의 콘텐츠를 크롤링하여 데이터베이스에 저장, 대용량 파일의 읽기/쓰기, 외부 API로부터 데이터 요청 및 응답 처리
구분의 중요성:
작업의 종류를 올바르게 구분하는 것은 최적의 성능을 달성하기 위해 적절한 기술과 전략을 선택하는 데 중요합니다. 예를 들어, CPU 바운드 작업에는 멀티프로세싱이나 코드 최적화가 효과적일 수 있으며, I/O 바운드 작업에는 비동기 프로그래밍이나 멀티스레딩이 더 적합할 수 있습니다.
'프로그래밍 언어 > 파이썬' 카테고리의 다른 글
매직메소드란? (0) | 2024.03.20 |
---|---|
MRO(Method Resolution Order) (0) | 2024.03.20 |
데코레이터란? (0) | 2024.03.20 |
코루틴이란? (0) | 2024.03.20 |
List와 Tuple의 차이 (0) | 2024.03.20 |