Application/Python

[Python] 파이썬 멀티스레딩으로 작업 속도 확 줄이기

devsalix 2026. 5. 29. 12:12
728x90

파이썬 멀티스레딩으로 작업 속도 확 줄이기

지난 글에서 async/await로 동시성을 다뤘잖아요. 그런데 사실 "여러 일을 동시에 처리한다"는 같은 목표를 가진 또 다른 방식이 있어요. 결이 꽤 다른 방식인데, 바로 멀티스레딩이라는 거예요.

async가 한 명의 직원이 일을 영리하게 굴리는 느낌이라면, 파이썬 멀티스레딩은 진짜로 여러 명을 동시에 투입하는 쪽에 가깝습니다. 처음엔 이 둘이 헷갈리실 수 있는데요. 하나씩 천천히 풀어볼게요.


스레드가 도대체 뭔가요

비유부터 잡고 갈게요. 식당 하나를 떠올려보세요. 식당 전체가 프로세스라면, 그 안에서 일하는 직원 한 명이 스레드예요.

직원이 한 명뿐이면 손님을 한 명씩 차례로 응대해야 하잖아요. 그런데 직원이 네 명이면, 동시에 네 손님을 받을 수 있겠죠. 멀티스레딩이라는 건 결국 이런 거예요. 프로그램 안에서 일하는 흐름을 여러 개로 늘리는 것.

특히 기다리는 시간이 많은 작업, 예를 들어 파일 다운로드나 네트워크 요청 같은 일에서 효과가 정말 큽니다.


일단 동기 코드부터 보여드릴게요

비교해야 효과가 와닿으니까 먼저 평범하게 짠 코드를 보여드릴게요.

import time

def 다운로드(파일):
    print(f"{파일} 시작")     # 작업 시작 알림
    time.sleep(2)             # 다운로드 대기를 흉내냄
    print(f"{파일} 완료")     # 끝났다고 알림

start = time.time()           # 시작 시각 기록
for f in ["a", "b", "c"]:     # 세 개 파일을 순서대로
    다운로드(f)
print(f"걸린 시간: {time.time() - start:.1f}초")

 

실행하면 이렇게 나옵니다.

a 시작
a 완료
b 시작
b 완료
c 시작
c 완료
걸린 시간: 6.0초

 

세 개를 줄 세워서 하나씩 하니까 6초가 걸리네요. 사실 각자 2초씩 기다리는 일인데, 굳이 줄 세울 필요가 있나 싶죠.


python threading 모듈로 동시에 굴리기

이제 스레드를 써서 세 개를 한꺼번에 돌려볼게요.

import threading
import time

def 다운로드(파일):
    print(f"{파일} 시작")
    time.sleep(2)
    print(f"{파일} 완료")

start = time.time()
스레드들 = []
for f in ["a", "b", "c"]:
    t = threading.Thread(target=다운로드, args=(f,))  # 스레드 객체 만들기
    t.start()                                          # 시작 (실행은 OS가 알아서)
    스레드들.append(t)

for t in 스레드들:
    t.join()  # 이 스레드 끝날 때까지 메인이 기다림

print(f"걸린 시간: {time.time() - start:.1f}초")

 

결과는 대략 이런 느낌이에요.

a 시작
b 시작
c 시작
a 완료
b 완료
c 완료
걸린 시간: 2.0초

 

6초가 2초로 줄었어요. 세 개가 동시에 기다리니까 기다리는 시간이 겹쳐진 거죠.

여기서 짚고 갈 게 두 가지 있어요.

첫째, args에 콤마. args=("a",)처럼 끝에 콤마를 꼭 붙여야 해요. 안 붙이면 파이썬이 그냥 문자열로 보고 한 글자씩 인자로 넘기려다 에러가 납니다. 저도 처음에 여기서 한참 헤맸어요.

둘째, join()의 역할. start만 하고 join을 안 부르면 메인 스레드가 먼저 끝나버려서 결과가 이상해질 수 있어요. "이 스레드 끝날 때까지 기다려"라고 말해주는 게 join이라고 생각하시면 됩니다.

아, 그리고 출력 순서가 매번 살짝 다르게 나올 수도 있어요. 이건 버그가 아니라 OS가 스레드를 마음대로 번갈아 실행하기 때문이거든요. 정상입니다.


ThreadPoolExecutor가 훨씬 깔끔해요

솔직히 위처럼 스레드를 손으로 만들고 리스트에 담고 join 돌리는 거, 좀 번거롭잖아요. 그래서 더 권장하는 방법이 있어요. concurrent.futures 모듈의 ThreadPoolExecutor예요.

from concurrent.futures import ThreadPoolExecutor
import time

def 다운로드(파일):
    print(f"{파일} 시작")
    time.sleep(2)
    return f"{파일} 완료"   # 결과를 돌려줄 수 있음

start = time.time()
with ThreadPoolExecutor(max_workers=3) as executor:   # 최대 3개 동시 실행
    결과들 = list(executor.map(다운로드, ["a.txt", "b.txt", "c.txt"]))

print(결과들)
print(f"걸린 시간: {time.time() - start:.1f}초")

 

with 블록이 끝나면 알아서 모든 스레드를 기다려주고 정리까지 해줘요. join 신경 쓸 일도 없고요. max_workers로 동시에 몇 개 굴릴지 정하고, map에 함수와 리스트를 넘기면 끝납니다.

처음 파이썬 병렬처리를 만지신다면, threading.Thread보다 이쪽을 먼저 익히시는 걸 권해드려요. 훨씬 마음이 편하거든요.


GIL, 솔직히 짚고 갈게요

여기까지 보시고 "오 그럼 무거운 계산도 스레드 4개 쓰면 4배 빨라지겠네?" 하실 수 있는데요. 솔직히 말씀드리면, 그건 아니에요.

파이썬에는 GIL(Global Interpreter Lock) 이라는 게 있어서, 한 번에 딱 하나의 스레드만 파이썬 코드를 실행하도록 막아둬요. 그래서 진짜로 4코어를 다 갈아 넣는 병렬 계산은 안 됩니다.

그런데 다행인 게, 네트워크 대기나 파일 읽기 같은 I/O 작업 중에는 GIL을 잠깐 풀어줘요. 그래서 다운로드, 파일 입출력, API 호출 같은 작업에서는 멀티스레딩 효과가 확실하게 납니다.

반대로 큰 숫자 계산이나 이미지 처리처럼 CPU만 빡세게 쓰는 작업은 별 효과가 없어요. 이때는 multiprocessing이라는 다른 모듈을 써서 진짜로 여러 프로세스를 띄워야 하는데, 그건 나중에 기회가 되면 다룰게요.


async랑 뭐가 다른가요

한 문장으로 비교하자면, async는 한 사람이 일들 사이를 바쁘게 오가는 방식이고, threading은 일하는 사람 수 자체를 늘리는 방식이에요. async는 양보 시점을 직접 await로 정해주는 반면, threading은 OS가 알아서 스레드를 번갈아 돌립니다. 둘 다 I/O 바운드 작업을 빠르게 만든다는 목적은 같아요.


자주 하는 실수 모음

처음 멀티스레딩 코드를 짤 때 거의 다 한 번씩 겪는 실수들이에요. 미리 알고 가시면 마음이 편할 거예요.

  • args=("hello") 처럼 콤마 빠뜨리기 (튜플이 아니라 그냥 문자열이 됨)
  • start()만 하고 join() 안 부르기 (결과 보기 전에 메인이 종료)
  • CPU 계산에 멀티스레딩 써놓고 "왜 안 빨라지지" 의아해하기 (GIL 때문에 그래요, 이럴 땐 multiprocessing)
  • 여러 스레드가 같은 변수를 동시에 수정해서 값이 꼬이는 경우 (race condition이라고 부르는데, 이럴 땐 threading.Lock()을 씁니다)

한번 직접 해보세요

각자 다른 시간이 걸리는 작업 세 개를 만들어보세요. 예를 들어 첫 번째는 1초, 두 번째는 3초, 세 번째는 2초 걸리는 함수요. 이걸 동기로 한 번, ThreadPoolExecutor로 한 번 돌려서 시간 차이를 직접 측정해보시면 효과가 확 와닿을 거예요.

힌트로 첫 줄만 드릴게요.

from concurrent.futures import ThreadPoolExecutor
import time

 

오늘 배운 ThreadPoolExecutor가 다음 글에서 진짜 빛을 발할 거예요. 다음에는 파이썬에서 가장 많이 쓰는 라이브러리 중 하나인 requests로 실제 인터넷 API를 호출해볼 건데요. 한 번에 여러 API를 동시에 부르는 실전 코드까지 함께 만들어볼 거니까, 오늘 내용 가볍게 손에 익혀두시면 좋아요.


 


제 글이 도움이 되셨다면 댓글 & 공감 부탁드려요 😀

 

 
728x90