포스트

[Asyncio] Python 동시성 관리 (2) - Python GIL

Python 동시성 관리를 위한 Python GIL 에 대한 정리


Post 에서 멀티 스레드가 오히려 단일 스레드 보다 시간이 오래 걸린 것을 확인할 수 있었다. 이는 GIL 때문인데 GIL 에 대해서 알아보도록 하겠다. GIL 을 정확하게 이해하기 위해 먼저 CPython을 먼저 이해해본다.

CPython

인터프리터(Interpreter)란 코드를 한 줄씩 읽으면서 실행하는 프로그램을 말한다. 이는 컴파일 언어와 비교하였을 때 프로그램 수정이 간단하고 생산성, 유지 보수 측면에서 좋다는 장점이 있다. 하지만 실행 시마다 소스 코드 한 줄씩 번역해야하므로 컴파일 언어보다 느리다는 단점을 가지고 있다.

이러한 Python 이 갖는 단점을 보완하고자 Python 인터프리터의 표준 구현체로 받아들여지고 있는 것이 CPython 이다.

Python 을 C 언어로 구현한 구현체를 의미하며, 실제 CPython 은 인터프리터이면서 컴파일러라고 볼 수 있다.

CPython 의 대표적인 단점은 GIL 특성이 있다. 즉, CPython을 내부 구현체로 사용되는 모듈이나 함수 등은 GIL 특성을 가지고 있다고 볼 수 있는 것이다.

GIL 이란?

Global Interpreter Lock 의 약자로 여러 개의 스레드가 파이썬 코드를 동시에 실행하지 못하도록 하는 것을 의미한다.

즉, GIL 은 프로세스당 하나의 기본 스레드만 실행될 수 있으며, 멀티코어 프로세서에서 실행되는 경우에도 하나의 스레드가 실행되는 매커니즘이다.

  • 아래의 그림은 GIL 매커니즘을 적용했을 때, 3개의 스레드가 수행되는 흐름을 쉽게 나타낸 그림이다.

  1. Thread1 이 GIL 을 획득한 채로 작업을 수행
  2. Thread1이 I/O 작업 완료 후 GIL을 해제
  3. Thread2 가 GIL 획득
  4. Thread2 가 I/O 작업 완료 후 GIL을 해제
  5. Thread3 가 GIL 획득
  6. Thread3 가 I?O 작업 완료 후 GIL을 해제

해당 주기는 각 스레드의 작업이 완료될 때 까지 주기적으로 반복된다. 중요한 포인트 중 하나는 I/O 시점에 GIL이 해제된다는 것이다.

GIL 의 목적

CPython 에서의 GIL 적용의 목적은 메모리 관리 시 발생할 수 있는 문제점을 해결하고자 함이다.

Python 은 메모리 관리 방법으로 reference counting 을 사용한다. Python에서의 모든 것들은 Object(객체) 이며 각 객체마다 레퍼런스로 사용된 횟수를 저장한는데, 이를 reference counting 이라고 한다. -> 이는 0이 되는 경우 메모리에서 해제된다.

1
2
3
4
5
6
7
8
import sys

a = []
b = a
sys.getrefcount(a)

# 결과
3

sys.getrefcount 함수로 확인할 수 있으며 위 코드에서 빈 배열([]) 은 a, b, sys.getrefcount 에 의해 참조되고 있으니 reference counting3인 것을 확인할 수 있다.

만약 여러 스레드가 동시에 reference counting 을 수정할 시에 race condition 이 발생할 수 있다.

이 문제를 해결하기 위하여 전역 인터프리터에 Lock 을 걸어 동시 접근할 수 없도록 제약을 걸어둔 것이다.

따라서 GIL을 적용한다면 race condition을 방지할 수 있는 장점이 있지만, 모든 CPU를 활용하는 프로그램이 멀티스레드로 병렬 처리를 할 수 없다는 단점이 있다.

또한 단일 스레드로 수행시 발생하지 않았던 스레드 간의 데이터나 정보를 주고 받는 (context switching) 비용이 더 발생하기 때문이다. -> 이 때문에 오히려 멀티 스레드 적용시의 성능이 단일 스레드에서 수행했을 때 보다 성능이 떨어지는 것이다.

Python 에서 Multi-Thread 성능이 높을 때

I/O bound 한 작업에서는 멀티 스레딩이 성능을 향상시켜 줄 수 있다. I/O bound 한 작업이란, I/O 작업이 주를 이루는 작업을 말한다.

I/O 작업이 주를 이루는 상황에서는 멀티 스레딩으로 I/O 작업 수행시에 다른 스레드에 작업을 할당할 수 있으므로, 성능이 향상될 수 있다. 반대로, 단일 스레드 환경에서는 I/O 작업 수행시에도 다른 작업을 수행할 수 없으므로 성능이 향상되지 않는 것이다.

아래 예제는 time.sleep 이 I/O 작업을 수행하는 것이라고 가정하고 시간을 측정한 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time
import threading
import random

list_len = 10_000_000

def working():
	[random.random() for _ in range(list_len)].sort()
	time.sleep(0.1)
	[random.random() for _ in range(list_len)].sort()
	time.sleep(0.1)
	[random.random() for _ in range(list_len)].sort()
	time.sleep(0.1)
	[random.random() for _ in range(list_len)].sort()
	time.sleep(0.1)
	[random.random() for _ in range(list_len)].sort()
	time.sleep(0.1)
	return True
  • 단일 스레드 환경
1
2
working()
working()
1
53.5 s ± 1.24 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
  • 멀티 스레드 환경
1
2
3
4
5
6
7
threads = []
for i in range(2):
	threads.append(threading.Thread(target=working))
	threads[i].start()

for t in threads:
	t.join()
1
52.1 s ± 862 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

GIL 을 우회하는 법

GIL 우회하는 법은 멀티프로세싱을 사용하여 병렬 처리를 적용하거나, Jython, PyPy 등의 GIL을 사용하지 않는 파이썬 구현체를 사용하면 GIL 을 우회할 수 있습니다.

요약

GIL은 프로세스 당 하나의 기본 스레드만 실행될 수 있으며, 멀티코어 프로세서에서 실행되는 경우에도 하나의 스레드가 한 시에 실행되는 메커니즘이다. GIL이 적용된 환경에서 멀티 스레딩은 단일 스레딩보다 무조건 성능이 낮아지는 것은 아니며, I/O bound 한 작업에서는 멀티 스레딩이 성능을 향상 시킬 수 있다.


참조 : https://velog.io/@jaebig/python-%EB%8F%99%EC%8B%9C%EC%84%B1-%EA%B4%80%EB%A6%AC-2-GILGlobal-Interpreter-Lock

Google AdSense — Post Ad
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

Comments powered by Disqus.