[Python] GIL & multi-thread

류지수·2025년 3월 11일

개발일지

목록 보기
5/5

파이썬는 Interpreter 언어이기 때문에 기본적으로 싱글 쓰레드에서 순차적으로 동작
→ 병렬처리를 하기 위해서 별도 로직을 구현해야함.

multi thread 구현 방법

  1. threading 모듈을 사용하여 구현 가능

  2. python의 GIL 때문에 I/O작업(파일 처리, 네트워크 요청)에서 효과적이며, CPU작업에는 multiprocessing이 적합

    GIL 이란?

    • GIL(Global Interpreter Lock)은 Python 인터프리터가 한 번에 하나의 스레드만 실행하도록 제한하는 메커니즘

    • 즉, Python에서 멀티쓰레드를 사용하더라도 한 번에 하나의 스레드만 실행 할 수 있도록 제한

      GIL이 존재하는 이유

    • python은 메모리 관리를 자동화하는 GC(Garbage Collection)을 사용

    • 특히 Referece Counting을 기반으로 객체의 메모리를 관리하는데,

    • 멀티쓰레드 환경에서는 여러 스레드가 동시에 메모리를 변경하면 데이터 충돌이 발생할 가능성이 있음

    • 이를 방지하기 위해 GIL을 사용하여 한 번에 하나의 스레드만 실행하도록 제한한 것

      GIL의 문제점

      GIL 때문에 멀티쓰레드가 CPU 연산을 병렬로 실행하지 못함

      즉, 여러 개의 CPU 코어가 있어도 한 번에 하나의 스레드만 실행되므로 멀티코어를 활용할 수 없음

      GIL 우회방법

    1. multiprocessing 사용 (GIL 영향 없음)
      • multiprocessing 모듈을 사용하면 프로세스를 여러 . 개실행하여 GIL의 영향을 받지 않음

      • 각 프로세스는 독립적인 메모리를 가지므로, CPU 코어를 100% 활용 가능

        import multiprocessing
        import time
        
        def cpu_task():
            count = 0
            for _ in range(10**7):
                count += 1
        
        start = time.time()
        processes = []
        
        for _ in range(4):  # 4개의 프로세스 생성
            p = multiprocessing.Process(target=cpu_task)
            p.start()
            processes.append(p)
        
        for p in processes:
            p.join()
        
        print("멀티프로세스 실행 시간:", time.time() - start)

        GIL이 없는 다른 언어 사용

      • Cython: C 언어로 컴파일하여 GIL을 우회할 수 있음

      • Jython, IronPython: GIL이 존재하지 않는 Python 구현체

      • Rust, C++ 등 GIL이 없는 언어 사용



1) ThreadPoolExecutor

  • concurrent.futures.ThreadPoolExecutor를 사용하여 간결하게 멀티쓰레드 구현
import concurrent.futures
import time

def worker(n):
		print(f"Task {n} 시작")
		time.sleep(2)
		print(f"Task {n} 완료")
		
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
		executor.map(worker, range(10))

장점

  • 코드가 간결하고 유지보수하기 쉬움
  • max_workers를 조절하여 스레드 개수 관리 가능

2) threading.Thread 사용

  • 직접 threading.Thread를 생성하여 실행
import threading
import time

def worker(n):
		print(f"Thread-{n} 시작"})
		time.sleep(2)
		print(f"Thread-{n} 완료"})
		
threads = []

for i in range(10):
		t = threading.Thread(target=worker, args=(i,))
		t.start()
		threads.appendD(t)
		
for t in thrads:
		t.join()
		
print("모든 작업 완료")

장점

  • ThreadPoolExecuter보다 더 유연한 설정 가능.
  • 특정 스레드를 개별적으로 컨트롤할 수 있음

단점

  • 코드가 길어질 수 있으며 관리가 어려울 수 있음

3. 멀티쓰레드 vs 멀티프로세싱

구분멀티쓰레드 (threading)멀티프로세싱 (multiprocessing)
실행 단위스레드(Thread)프로세스(Process)
GIL의 영향O (GIL 때문에 CPU 작업에 비효율적)X (각 프로세스마다 별도 메모리 공간 사용)
적합한 작업네트워크, 파일 I/OCPU 연산 (데이터 분석, AI 연산 등)
메모리 사용공유 메모리별도 메모리 공간 사용

🔹 I/O 작업(파일 읽기/쓰기, 네트워크 요청)threading (멀티쓰레드)

🔹 CPU 연산이 많은 경우multiprocessing 사용 추천

4. 주의할 점

  • python은 GIL 때문에 멀티쓰레드가 CPU 연산에 비효율적임.
  • 여러 스레드가 동시에 실행되므로 threading.Lock()을 사용하여 공유 데이터 접근을 동기화해야함.
import threading

lock = threading.Lock()

def critical_section():
		with lock:
				# 안전한 공유 데이터 접근
				print("공유 데이터 접근")

결론

  • I/O 작업(파일 처리, 네트워크 요청)에는 ThreadPoolExecutor를 추천
  • CPU 연산이 많다면 multiprocessing 사용
  • threading.Lock()으로 동기화를 고려해야함.

회고
thread관리가 잘못되면 세션 관리가 안되고, 필요할 때 세션이 종료되림.
(thread, queue 관리 필요)

profile
끄적끄적

0개의 댓글