파이썬에서 멀티 스레드를 잘 사용하지 않습니다. 대부분 멀티 프로세싱을 사용합니다. 그 이유가 뭘까요?
핵심만 먼저 말하자면, 병렬성이 제대로 이뤄지지 않기 때문입니다.
CPU 코어가 남는 경우, 스레드는 병렬적으로 실행되게 됩니다. 반대로 코어가 하나만 있다면 순차적으로 실행됩니다. 그런데 파이썬은 어떠한 조건에서도 멀티 스레드가 순차적으로 실행되는 모습을 보여줍니다.
한번 테스트를 해볼까요?
Python 멀티 스레드 테스트
import threading
import time
def thread():
start = time.time()
a = 1
for i in range(10000000):
a += i
print(threading.current_thread().name,":", time.time() - start)
# 일반 실행
thread()
# 스레드 실행
MAX_THREADS = 3
threads = [threading.Thread(target=thread) for _ in range(MAX_THREADS)]
start = time.time()
for t in threads:
t.start()
for t in threads:
t.join()
print("Total Thread Execution Time: ", time.time() - start)
결과
일반 실행에 0.6초가 걸리는 함수가 있습니다.
이 함수를 스레드 3개로 실행하면 일반적으로는 각 실행시간이나 총 실행시간이나 0.6초에서 크게 차이 나지 않는 선이어야 할 겁니다. 그런데 정직하게 0.6초의 3배인 1.8초가 걸리는 것을 확인할 수 있습니다. 이는 모든 스레드가 하나의 코어에서 순차적으로 실행된다는 의미로 볼 수 있습니다.
그럼 자바는 어떻게 작동하는지 확인해 볼까요?
Java 멀티 스레드 테스트
import java.util.List;
import java.util.ArrayList;
public class Main {
static class Timer {
private final Long start;
public Timer(){
start = System.currentTimeMillis();
}
public void end(String prefix){
Long end = System.currentTimeMillis();
double seconds = ((double) (end - start) / 1000);
System.out.println(prefix + seconds);
}
}
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
Timer timer = new Timer();
long a = 1;
for (long i = 1; i < 100000000; i++){
a+=i;
}
timer.end(Thread.currentThread().threadId() + ": ");
};
runnable.run();
int MAX_THREADS = 3;
List<Thread> threads = new ArrayList<>();
for (int i=0; i < MAX_THREADS; i++){
threads.add(new Thread(runnable));
}
Timer timer = new Timer();
for (Thread thread: threads){
thread.start();
}
for (Thread thread: threads){
thread.join();
}
timer.end("Total Thread Execution Time: ");
}
}
결과
일반 실행은 0.038초가 걸렸네요.(파이썬에 비해 확실히 빠르네요) 그리고 스레드 3개에서 실행도 역시 0.037초, 총 실행시간도 0.037초 인 것을 보면 일반 실행이랑 같다고 볼 수 있네요. 파이썬과는 다르게 정말 병렬적으로 실행되는 것을 확인할 수 있습니다.
이유가 뭘까?
파이썬이 멀티 스레드가 병렬적으로 실행되지 않고 순차적으로 실행되는 이유는 뭘까요? 이는 파이썬의 GIL 때문입니다. GIL은 Global Interpreter Lock의 약자로, 스레드가 병렬적으로 실행되지 않도록 막는 역할을 합니다.
즉, 하나의 스레드가 실행 중이면, 다른 스레드는 실행될 수 없다는 뜻이죠.
그렇다면 왜 GIL이 있는 것일까요?
이는 파이썬의 GC(Garbage Collection) 방식 때문입니다. 파이썬의 GC는 Reference Count라는 것으로 진행하는데요. 이는 자기 자신이 몇 번 할당되었는지 판단하는 값입니다. 이 값이 0이라면 메모리에서 해제됩니다.
객체에 대한 RC값을 올리고 내릴 때 Lock이 걸려있지 않다면 스레드가 여러 개일 때 Race Condition 문제가 무조건 발생할 겁니다. 그래서 Lock을 걸게 되었고, 그것이 바로 GIL인 것이죠.
자바는 Mark And Sweep 방식으로 현재 사용되는 객체만 직접 탐색하기 때문에 이런 문제가 없습니다.
'Python' 카테고리의 다른 글
Python WSGI 서버 직접 만들어보기 (0) | 2025.02.03 |
---|---|
Python WSGI란? (0) | 2025.02.03 |
Python 추상클래스 만들기 (0) | 2025.02.03 |