
도대체 파이썬은 왜 쓰레드를 하나만 사용할 수 있는거지?
이런걸 왜 만들어놨을까?
멀티 프로세스가 애플리케이션 단위의 멀티 태스킹이라면,
멀티 스레드는 애플리케이션 내부에서 멀티 태스킹이라고 볼 수 있습니다.
멀티 스레딩는 이처럼 N개의 스레드를 통해
병렬성으로 작업을 실행하여 시간을 단축하는 것을 의미한다.
보통이라면... 맞다.
파이썬은 GIL(Global Interpreter Lock) 때문에
프로세스 당, 싱글스레드로 제한한다.
그럼 왜 이러한 Lock이 걸려있는 걸까?
GIL(Global Interpreter Lock)
다수의 스레드가 동시에 파이썬을 실행하지 못하게 막는 일종의 뮤텍스(Mutex)
GIL이 스레드끼리 공유하는 프로세스의 자원을 이름 그대로 Global 하게 Lock 해버리고,
하나의 스레드에만 이 자원에 접근하는 것을 허용한다.
그림과 같이 멀티스레드라 하더라도, 한 번에 하나의 스레드만 실행하게 된다. (동시성)
이는 스레드 간에 컨텍스트 스위칭 비용을 발생시키고,
멀티스레드가 싱글스레드와 비슷한 성능을 보이거나 오히려 떨어지게 되는 결과를 보여준다.
[ST]Time taken in seconds: 3.298...(생략) # 싱글 스레드 (1T)
[MT]Time taken in seconds: 3.268...(생략) # 멀티 스레드 (2T)
이런 Lock이 생긴 이유는,
파이썬의 가비지 컬렉션(garbage collection, GC) 때문이다.
더 이상 사용되지 않는 동적으로 할당된 메모리를 찾아 해제하여
메모리를 효율적으로 관리하는 기능
파이썬에서 모든 객체는 자신을 가리키는 참조 횟수(reference count)를 저장하고 있으며,
그 값이 0에 도달하면 해당 객체가 가진 메모리를 회수하게 된다.
import sys
class Foo:
def __del__(self):
print("Removed!")
# sys.getrefcount(obj)
# 그 객체가 지금 몇 번 참조되고 있는지
a = Foo()
sys.getrefcount(a) # 2 -> 인자로 넘긴 a까지 포함.
b = a
sys.getrefcount(a) # 3 -> b도 a를 참조
del b
sys.getrefcount(a) # 2
del a # Removed! 출력
만약 여러 쓰레드가 동일한 객체를 사용한다면,
여러 스레드가 참조 횟수 변수에 동시에 접근하면 경쟁 상태(Race Condition)가 발생하게 된다.
이로 인해 최악의 경우에는 메모리 누수(memory leak)가 발생하거나,
잘못 회수하게 될 수도 있기 때문에 모든 객체에 대해 락(lock)을 추가하여 thread-safety를 보장해야 한다.
이를 위해 파이썬은 자원을 공유할 수 없도록
한 개의 스레드만 코드를 실행하도록 Lock을 해놨다.
이것이 GIL이 필요한 이유다.
파이썬에서 멀티스레딩이 아예 의미 없는 것은 아니다.
CPU 연산이 큰 비중을 차지하는 경우,
멀티스레딩이 문맥교환(Context Switch)이 일어나면서 싱글스레드보다 성능이 떨어지게 된다.
물론, '파이썬 코드를 통한 CPU 연산'만, GIL이 적용된다!

파이썬의 코드를 통해 CPU 연산을 하는 작업만 GIL로 제한이 되어 있을 뿐,
I/O, DB 입출력, 네트워크 통신 등 대기 시간이 큰 작업에서는 한 스레드가 응답을 기다리는 동안 다른 스레드가 실행될 수 있어 멀티스레딩의 효과가 잘 나타난다.
즉, 멀티스레딩은 CPU를 더 빨리 계산하게 해주는 게 아니라,
대기 시간을 겹쳐게 처리해서, 전체 처리량과 응답을 개선하는 데 유리하다.
멀티스레딩을 효율적으로 사용할 수 있는 라이브러리가 파이썬에 존재한다.
async / await 구문을 사용하여 동시성 코드를 작성하는 라이브러리
[참고] https://docs.python.org/ko/3.12/library/asyncio.html
import asyncio, time
async def job(name, n):
for i in range(1, n + 1):
print(f"{name}{i} 시작")
await asyncio.sleep(1) # 여기서 양보 -> 다른 작업이 실행됨
print(f"{name}{i} 완료")
async def main():
t0 = time.perf_counter()
await asyncio.gather(job("A", 3), job("B", 2))
print("총시간:", round(time.perf_counter() - t0, 2), "초")
if __name__ == "__main__":
asyncio.run(main())
기존 def 키워드 앞에 async 키워드까지 붙이면, 이 함수는 비동기 처리된다.
이러한 함수를 파이썬에서는 코루틴(coroutine)이라고 부른다.
이러한 비동기 함수는 호출하면 coroutine 객체가 리턴된다.
비동기 함수는 일반적으로
async로 선언된 다른 비동기 함수 내에서 await를 붙여서 호출해야 한다.

3.13 버전에서는 GIL-Free 멀티스레딩 기능을 실험적으로 도입했다.
3.14는 멀티 인터프리터(Multiple Interpreters)와 이를 쉽게 활용할 수 있는 InterpreterPoolExecutor가 정식으로 도입되었다.
이 기능은 GIL(Global Interpreter Lock) 한계를 위해 제거하고,
CPU 집약적인 작업을 병렬로 실행할 수 있는 새로운 방식이다.