쓰레드를 사용하여 2가지 동작이 동시에 실행되는 코드를 작성해보자.
① threading
② time
③ thread_1
④ t1
⑤ 메인 쓰레드
이제 총 두 개의 쓰레드로 나누어져 코드가 동작하게 되었다. 실행결과를 확인해보자.
2초마다 한번씩 "메인 쓰레드 동작"을 출력하고, 1초마다 한번씩 "쓰레드 1 동작"이 출력된다.
위 코드에서 쓰레드 1은 메인 쓰레드의 동작 여부와 관계 없이 항상 동작한다. 이 사실을 확인하기 위해 디버그 모드로 위 파일을 실행한 후, Ctrl+C로 메인 쓰레드를 종료시켜보겠다.
메인 쓰레드가 종료된 이후에도 쓰레드 1이 계속해서 동작하고 있음을 확인할 수 있다.
그러나 일반적으로는, 메인 쓰레드가 종료될 때 생성된 모든 쓰레드도 함께 종료되게 하는 것이 프로그램의 안정성과 자원 관리에 유리하다. 이렇게 메인 쓰레드가 종료될 때 함께 종료되도록 설정된 쓰레드를 데몬 쓰레드라고 부른다. 아래와 같이 코드를 수정한 후 결과를 비교해보자.
위의 코드에서 t1.daemon = True 한 줄만 추가해주었다. 즉, 쓰레드 객체 t1을 데몬 쓰레드로 설정해준 것이다. 이번에도 디버그 모드로 실행한 후, Ctrl+C로 강제 종료시켜보겠다.
메인 쓰레드 종료와 동시에 쓰레드 1도 함께 종료되었다.
이번에는 두 개의 쓰레드를 사용해보자. 또한 쓰레드 위에서 돌아가는 메서드에 입력 값도 넣어주자. 참고로, 메서드에 입력 값을 넣으려면 args 속성을 사용해야 한다.
실행결과는 아래와 같다.
원래대로라면, 위의 실행결과는 1번 쓰레드와 2번 쓰레드, 메인 쓰레드가 경쟁적으로 실행되면서 결과의 순서가 뒤엉켰어야 한다. 그러나 예상과 달리 거의 순차적으로 출력되었다. 그 이유는 GIL(Global Interpreter Lock) 메커니즘이 작용했기 때문이다.
GIL이란, 한 번에 하나의 파이썬 쓰레드만 Python 바이트 코드를 실행하도록 제한하는 기능이다. 따라서 Python에서는 여러 개의 쓰레드가 동시에 실행되더라도, 파이썬 바이트 코드는 한 번에 하나의 쓰레드에서만 실행하게 된다.
그래서 실제로 위와 같은 코드는 멀티 쓰레드 방식을 사용하면 오히려 싱글쓰레드보다도 더 느려진다. 그럼 코드도 복잡하고, 심지어 속도도 더 느린 멀티 쓰레드를 왜 쓰는건지 궁금할 것이다.
멀티쓰레드가 유리한 상황은 대표적으로 I/O-bound 작업을 처리할 때이다. 여기서 I/O-bound 작업이란, 파일 입출력, 네트워크 통신, 데이터베이스 쿼리 등이 포함될 수 있다. 이러한 작업은 CPU보다는 입출력 장치에 의한 제한을 받기 때문에, CPU가 대기 상태인 시간이 많다.
이러한 상황에서 멀티쓰레딩을 사용하면, I/O-bound 작업을 동시에 여러 개의 쓰레드로 처리할 수 있게 된다. 즉, 하나의 쓰레드가 I/O 작업을 수행하는 동안 다른 쓰레드가 CPU를 활용하여 다른 I/O 작업을 처리할 수 있게 되는 것이다. 따라서 I/O-bound 작업을 병렬로 처리하면 대기 시간을 최소화할 수 있으므로, 전체적인 프로그램의 실행 시간이 단축된다.
그러므로 위에서 실습한 코드는 단지 멀티 쓰레드를 사용하는 방법을 알아보기 위한 코드였을 뿐, 실용적이지는 못한 코드이다.