[ Python ] multi-threading, multi-processing

Lutica_·2025년 8월 31일
0
post-thumbnail

Python 에서 잘 언급되지 않던 멀티 프로세싱/스레딩에 대하여

  • Python은 인터프리터 언어로, 보통 멀티프로세싱과 스레딩을 언급해야 할 정도까지 가는 일은 많지 않았다.
  • 그러나 최근 AI의 대두와 개발의 편의성을 생각하면, 시스템과 AI의 통합을 위해 그정도까지 가는 경우가 종종 생기기 시작했다.
  • 따라서 파이썬도, 이제 시대에 맞추어 멀티스레딩과 프로세싱을 지원해가기 시작한다.

Multi-Processing과 Multi-Threading의 차이에 관하여

  • 멀티프로세싱은 메모리를 따로 가지고, 멀티스레딩은 메모리를 동일하게 공유한다.
  • Multi-Processing는 기존에 가지고 있던 값들을 복사해야 할 의무가 있다.
  • 따라서, process가 fork될 때, Context-Switching 비용이 생긴다.

Multi-Threading 에서의 제약사항과 그 차이

  • Python 3.13 이전까지는 GIL이라는 규약이 해제 불가능했다.

    단 한 가지 방법이 있다. 멀티스레딩을 지원하는 언어의 컴파일된 라이브러리를 쓰면 된다. FFI를 참조하라.

  • 그러나 3.13 이후부터 free-threaded JIT를 지원하기 시작했다.

  • GIL, Global Interpretrior Lock이란, 하나의 인터프리터는 무조건 하나의 Thread가 실제 실행되며, 멀티스레딩으로 표현되는 API는 실제로는 시분할로 작동한다는 사실을 의미한다.

    (컴퓨터공학중 운영체제에서) 시분할이라고 함은, 시간을 기준으로 Task를 나눠서 하나의 처리자로 여러 태스크에 컴퓨팅 파워를 쏟아붇는 과정등을 이야기한다.

  • 따라서, free-threaded가 적용되지 않은 경우라면, 기존 multi-Thread의 문제가 재현되지 않을 것이다.
    (임계영역 문제가 재현되지 않는 이유도 GIL때문이다.)

  • 자세한 방안은 여기를 참조하자. 설치방법은 여기 있다.

AsyncIO와 연관성

  • 기본적으로, 대부분의 문제는 AsyncIO라면 많이 해결 될 수 있다.

    대부분의 웹 문제는 통신에서 시간을 낭비하는 경우가 많기 때문이다.

  • AsyncIO, 비동기 입출력과 멀티스레딩, 프로세싱은 서로 다르다. 이는 하단의 표로 정리하자.
구분AsyncIO멀티스레딩 (Multithreading)멀티프로세싱 (Multiprocessing)
핵심 개념단일 스레드, 이벤트 루프 기반의 비동기 처리한 프로세스 내에서 여러 스레드가 실행 흐름 공유여러 프로세스가 독립적으로 실행
실행 단위태스크 (Task)태스크를 처리하는 단위,스레드 (Thread)프로세스 (Process)
메모리 공유✅ 단일 스레드 내에서 메모리 공유✅ 같은 프로세스 내 스레드 간 메모리 공유❌ 독립된 메모리 공간 (IPC 필요)
GIL의 영향⚠️ 받지 않음 (단일 스레드이므로)⛔️ 받음 (동시에 한 스레드만 Python 코드 실행)✅ 받지 않음 (프로세스별 GIL)
주요 장점매우 가볍고, 문맥 교환 비용이 거의 없음메모리 공유가 쉬워 데이터 교환이 간단진정한 의미의 병렬 처리 가능
주요 단점CPU 집약적 작업에 부적합GIL 때문에 CPU 집약적 작업에서 성능 향상 제한적메모리 사용량이 크고, 프로세스 생성/통신 비용이 높음
적합한 작업I/O 바운드 (네트워크 통신, 파일 입출력 등)I/O 바운드 (단, GIL 우회 가능한 C 확장 라이브러리 사용 시 CPU 바운드도 일부 가능)CPU 바운드 (복잡한 연산, 데이터 분석, 비디오 인코딩 등)
  • 아래는 AsyncIO의 예제이다.
import asyncio
import time
import aiohttp # 비동기 HTTP 요청을 위한 라이브러리 (pip install aiohttp)

# I/O 바운드 작업을 시뮬레이션하는 비동기 함수 (Coroutine)
async def fetch_url(session, url):
    """지정된 URL에서 데이터를 비동기적으로 가져옵니다."""
    print(f"요청 시작: {url}")
    try:
        # 'await'를 통해 네트워크 응답을 기다리는 동안 이벤트 루프는 다른 작업을 처리함
        async with session.get(url) as response:
            data = await response.text()
            print(f"요청 완료: {url}, 길이: {len(data)} bytes")
            return f"{url} 로드 성공"
    except Exception as e:
        print(f"오류 발생: {url}, {e}")
        return f"{url} 로드 실패"

async def main():
    """메인 실행 함수"""
    urls_to_fetch = [
        "https://www.google.com",
        "https://www.naver.com",
        "https://www.python.org",
        "https://github.com",
        "https://news.ycombinator.com",
    ]

    start_time = time.time()

    async with aiohttp.ClientSession() as session:
        # 각 URL에 대한 비동기 작업을 생성
        tasks = [fetch_url(session, url) for url in urls_to_fetch]
        # asyncio.gather를 사용하여 모든 작업이 완료될 때까지 동시에 실행
        results = await asyncio.gather(*tasks)
        print("\n--- 모든 작업 완료 ---")
        print(results)

    end_time = time.time()
    print(f"\n총 실행 시간: {end_time - start_time:.2f} 초")

if __name__ == "__main__":
    # 이벤트 루프를 시작하여 main 코루틴을 실행
    asyncio.run(main())

이제, 어디에 쓸 것인가?

  • 파이썬의 위상이 나날이 커져가면서, 각종 스크립트에서 파이썬을 보는 것은 어렵지 않은 일이 되었다. 심지어, PG/PLSQL마저도 Python으로 동작하는 구문이 있을 정도면 말을 다 한 것 같다.
  • 외부 통신이 매우 시간이 길고, 별도로 Context-switching을 하지 않아도 되는 경우라면 AsyncIO를 쓰고,
  • Multi-threading이 필요한 경우는 사실 대부분 AsyncIO로 정리 될 수는 있지만, 이미지 청킹등에 필요할 수 있다.
  • Multi-Processing은 서로 독립일 때 하여야 한다. 매우 중요하다.
profile
해보고 싶고, 하고 싶은 걸 하는 사람

0개의 댓글