본문 바로가기

Minding's Programming/FastAPI

[FastAPI] 파이썬 멀티 스레딩 사용해보기

728x90
반응형

해당 글은 인프런의 ' 파이썬 동시성 프로그래밍 : 데이터 수집부터 웹 개발까지 (feat. FastAPI)'을 수강하며 정리한 글이다.

 

 

파이썬 멀티 스레딩

우선 멀티 스레딩이란, 이전 글에서 정리한 '동시성'의 개념에서 비롯된 프로그래밍 개념이다. 하나의 프로세스 내에서 여러 개의 스레드가 동시에 작업을 실시해 속도 향상 등의 성능 개선 효과를 기대할 수 있다. 각 스레드가 독립적으로 실행하지만 자원을 공유하기 때문에, 효율적이면서도 충돌 가능성의 위험이 있다.

 

싱글 스레딩의 예시

# 파이썬 싱글스레딩 예시
import requests
import time
import os
import threading


def fetcher(session, url):
    print(f"{os.getpid()} process | {threading.get_ident()} url : {url}")
    with session.get(url) as response:
        return response.text


def main():
    urls = ["https://apple.com", "https://github.com"] * 50

    with requests.Session() as session:
        result = [fetcher(session, url) for url in urls]
        print(result)


if __name__ == "__main__":
    start = time.time()
    main()
    end = time.time()
    print(end - start)  # 34초

>>>
...
4708 process | 10620 url : https://apple.com
4708 process | 10620 url : https://github.com
4708 process | 10620 url : https://apple.com
...
34.045559883117676

위 코드는 apple.com과 github.com에 GET 요청을 보내 응답을 받는 코드다. os.getpid() 메서드를 통해 현재 프로세스 ID값을 얻고, threading.get_ident() 메서드를 통해 현재 스레드의 ID값을 알 수 있다.

 

코드의 결과값을 보면, 4708이라는 동일한 프로세스 안에서 ID가 10620이라는 스레드 홀로 작업 중인 것을 알 수 있다. 이 코드의 경우 하나의 작업을 모두 마칠 때까지 다음 작업으로 넘어가지 않는다. 해당 코드를 모두 실행하는데 약 34초가 걸렸다. (시간은 컴퓨터, 인터넷 환경에 따라 다를 수 있음을 참고)

 

코루틴을 사용한 동시성 프로그래밍(싱글스레딩)

# 파이썬 코루틴 사용한 동시성 프로그래밍
import aiohttp
import time
import asyncio
import os
import threading


async def fetcher(session, url):
    print(
        f"{os.getpid()} process | {threading.get_ident()} url : {url}"
    )  # 현재 프로세스의 ID와 사용 중인 스레드의 식별자를 출력
    async with session.get(url) as response:
        return await response.text()


async def main():
    urls = ["https://apple.com", "https://github.com"] * 50

    async with aiohttp.ClientSession() as session:
        result = await asyncio.gather(
            *[fetcher(session, url) for url in urls]
        )  # 언패킹
        print(result)


if __name__ == "__main__":
    start = time.time()
    asyncio.run(main())
    end = time.time()
    print(end - start)  # 14초
    
>>>
...
1952 process | 8012 url : https://apple.com
1952 process | 8012 url : https://github.com
1952 process | 8012 url : https://apple.com
1952 process | 8012 url : https://github.com
...
14.305680751800537

다음은 코루틴을 사용해 동시성 프로그래밍을 했을 때의 예시이다. 코루틴은 하나의 스레드에서 비동기 작업을 실행할 수 있게 도와준다.

 

코루틴을 사용했을 경우 동일한 프로세스에서 하나의 스레드를 사용하지만 I/O 바운드 작업의 대기시간 동안 다른 작업을 할 수 있기 때문에 위 코드보다 20초 정도 절약된 약 14초의 결과값을 보여준다.

 

멀티 스레딩의 예시

하지만 해당 프로그램의 작업 환경이나, 보안 등의 이유로 코루틴을 사용하지 못할 때가 있다. 그럴 때 동시성 프로그래밍을 하기 위해서는 멀티 스레딩을 사용할 수 있다.

# aiohttp에서 제공하는 메서드를 사용할 수 없을 때
# 동시성 프로그래밍을 하기 위해서는 멀티 스레딩을 사용하면 됨
# 관련 문서: https://docs.python.org/ko/3/library/concurrent.futures.html

import requests
import time
import os
import threading
from concurrent.futures import ThreadPoolExecutor


def fetcher(params):
    session = params[0]
    url = params[1]
    # map함수를 통해 tuple 형태로 인자가 전달되므로 다시 나누어줘야 함
    print(f"{os.getpid()} process | {threading.get_ident()} url : {url}")
    with session.get(url) as response:
        return response.text


def main():
    urls = ["https://apple.com", "https://github.com"] * 50

    executor = ThreadPoolExecutor(
        max_workers=10
    )  # max_workers: 최대 실행할 스레드의 개수

    with requests.Session() as session:
        # result = [fetcher(session, url) for url in urls]
        # print(result)
        params = [
            (session, url) for url in urls
        ]  # fetcher함수에는 session과 url 모두 인자로 들어가야 하기 때문에 새로운 리스트 만들어줌
        result = list(executor.map(fetcher, params))
        # map(): 인자값으로 실행시킬 fetch 함수와 파라미터에 대한 리스트를 넣어주면 된다.\
        # 함수와 각 파라미터 값(리스트 형태)을 매핑 시켜주는 함수
        print(result)


if __name__ == "__main__":
    start = time.time()
    main()
    end = time.time()
    print(end - start)
    # 스레드 1개: 36초
    # 스레드 10개: 22초

>>>
8944 process | 14196 url : https://apple.com
8944 process | 8920 url : https://github.com
8944 process | 4164 url : https://apple.com
8944 process | 10428 url : https://github.com
8944 process | 8920 url : https://apple.com
8944 process | 10428 url : https://github.com
8944 process | 7492 url : https://apple.com
8944 process | 10428 url : https://github.com
8944 process | 2060 url : https://apple.com
8944 process | 10428 url : https://github.com
8944 process | 10428 url : https://apple.com
8944 process | 6744 url : https://github.com
8944 process | 6744 url : https://apple.com
...
22.116799354553223

위 코드의 결과 값을 보면, 동일한 프로세스 내에서 여러 개의 ID를 가진 스레드를 볼 수 있다. ThreadPoolExecutor()라는 메서드 내의 max_workers라는 파라미터를 통해 스레드의 개수를 조절할 수 있는데, 위 코드에서는 10개의 스레드를 사용했을 때 22초의 시간이 걸렸다.

 

1개의 스레드를 사용했을 때에는 일반 코드보다 시간이 더 걸린 것을 확인할 수 있는데, import 해야 할 라이브러리도 늘어나는 등 스레드를 지정해주는 비용때문이다.

 

멀티 스레딩을 사용할 때는 위와 같이 스레드를 만들고 우선 순위를 지정해주는 등의 비용이 들기 때문에 메모리 점유율이 올라간다. 따라서 특별한 상황이 아니라면 코루틴을 사용하는 것을 더 권장한다고 한다.

728x90