본문 바로가기
Python/Python Advanced

11. Python - Thread를 사용한 여러 기능 동시 실행(1)

by Rosmary 2024. 3. 22.
728x90
반응형

 

 

 

 

 

 

 

최근 필자는 완성하지 못한 개인 프로젝트를 싸그리 중단하고 from the scratch로 다시 하나씩 코드를 작성해나가는 중이다(Linux 제어 코드부터 진행중이다. PEP8 Convention도 맞추고, 몇 개월 지나면 알아볼 수도 없는 코드도 줄일 겸).

 

필자가 진행 중인 프로젝트 중 하나를 잠깐 소개하자면, Linux로 인가받지 않은 사용자가 로그인을 시도하는 경우 해당 침입자의 IP를 firewalld에 Drop 정책으로 등록하는 과정을 실시간 + 자동으로 진행하도록 만드는 프로그램의 제작이다(필자가 만들고자하는 프로그램과 유사한 프로그램이 오픈 소스로 있었는데 이름을 잊어버렸다...).

 

Linux에 로그인을 시도하는 사용자의 정보는 보통 /var/log/secure 파일에 저장된다.

 

 

위의 스크린 샷은 VM에서 구동중인 Linux라 필자의 로그인 정보만 나타나는데, 실제 인터넷 선을 물린 Linux 노트북에는 전국 8도 뿐만 아니라 세계 각지의 해커들이 시도 때도 없이 Linux로 접속 시도를 하는 것이 탐지된다. 그리고 그 정보는 고스란히 /var/log/secure에 기록되고 말이다.

 

그렇기 때문에 필자가 제작하려는 프로그램은

 

---

1. /var/log/secure 파일의 정보를 "실시간"으로 읽어야하고,

2. 파일로부터 읽어들인 결과를 잘 가공하여 방화벽 정책을 만들어서

3. 정책을 방화벽에 적용하고, 잘 적용되었는지 검증하는

---

 

세 가지의 절차가 동시에 진행되어야한다. 

 

프로그래밍 언어를 막 접한 상태라면, 이 기능을 하나의 프로그램으로 만들기가 상당히 막막할 것이다. 참고서 등에 작성된 코드들은, 코드 내용을 순차로 처리하기만 할 뿐 위와 같이 병렬처리하는 예제를 Thread에 대해서 설명하지 않는 이상 보여주지 않기 때문이다.

 

...

 

맞다. 사실 Thread라는 말을 꺼내기 위해 약간의 뻘소리(?)를 했다. Thread가 하나의 프로그램에서 여러 기능을 수행할 수 있도록 만들어주는데, 오늘은 Python에서 Thread의 사용법에 대해 포스팅 할 예정이다. 필자도 Thread 쓴 지가 오래되어 정리도 할 겸 말이다.

 

** Java의 Thread는 이 포스팅을 참고하면 된다.

 

 

1. Thread 모듈

 

** 시작하기에 앞서, Thread와 메모리 동작은 위에 링크 걸어놓은 포스팅의 서두 부분을 참고하면 된다. 

 

먼저, 아래의 코드를 보자.

 

 

 

홀수와 짝수를 출력하는 함수가 존재하고, 필자는 이 함수를 __main__ 부분에서 순차로 호출하는 방식으로 코드를 실행했다. 당연한 이야기지만, count_even() 함수가 종료되기 이전까지는 count_odd() 함수가 실행되지 않는다. Call Stack 메모리에 먼저 적재된 count_even() 함수가 실행 및 종료로 Call Stack에서 사라져야만 count_odd() 함수가 Call Stack 메모리에 올라가고 실행되기 때문이다.

 

링크의 포스팅을 보시고 오신 분들이라면, "Python은 Java처럼 Thread를 제공하는 모듈이 없나요?"라고 물어보실 듯 한데, 당연히 있다.  Python의 Thread 역시 Java와 마찬가지로 Call Stack을 논리적으로 나누어 실행 함수나 코드를 적재하도록 만들기 때문에 Java의 Thread와도 사용 방법에 큰 차이는 없다. 똑같이 Thread 인스턴스 생성하고, 매서드로 이들 Thread를 제어한다.

 

그럼, Python의 Thread 관련 모듈을 사용하여 위의 두 코드가 동시에 실행되도록 만들어보자.

 

 

 

 

2. Python Thread 사용하기

 

(1) threading.Thread import

 

Python 에서 Thread 기능을 사용하려면 threading이라는 모듈을 import 해야한다. Python의 내장 모듈이라 pip를 통해 다운로드를 받거나 할 필요없이 바로 import를 진행하면 된다. 혹은 Thread 클래스만 사용하고자 한다면 아래와 같이 from import 문을 통해 Thread 클래스만 사용하도록 설정할 수 있다. 아직은 Thread에 Lock을 걸거나 하지 않을 예정이기 때문에 단순히 Thread 클래스만 import 했다.

 

 

 

 

(2) Thread 인스턴스 생성하기

 

import로 Thread 클래스의 사용이 가능해졌다면, 홀수 및 짝수 출력 함수에 대해 각각의 Thread를 생성해주어야 한다. 공식 docs 자료에 따르면, Thread 클래스의 인스턴스 생성은 아래의 포맷으로 진행한다.

 

class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

 

-  group : 개별 Thread를 묶어 그룹으로 관리하고자 할 때 사용한다.

-  name : 생성한 개별 Thread의 이름을 지정한다. 

-  target:  개별 Thread로 동작할 함수나 매서드 명을 입력한다.

-  args  :  개별 Thread의 target이 필요로하는 인자를 List나 Tuple 형태로 제공한다. 

 

크게 어려운 내용은 없다. 단, args의 경우, Tuple 사용과 함께 단일 인자만 입력받을 경우 Python이 Tuple 자료형임을 인지하지 못하기 때문에 반드시 뒤에 쉼표를 추가해주어야 한다.

 

짝수 및 홀수 출력을 담당하는 함수에 대해 Thread 인스턴스를 생성하는 코드는 아래와 같이 나타낼 수 있다.

 

 

 

Thread 클래스로 생성된 인스턴스는 의외로 인스턴스에 대한 정보를 많이 제공하지 않는다. 끽 해봐야 이름과 Thread가 살아있는지의 여부만 알 수 있다. 그리고 이름의 경우에도  getName()이라는 Java스러운 이름의 함수를 사용하지 않고도 name 멤버 변수로의 직접 접근이 가능하다는 특징이 있다.

 

 

 

(3) Thread 실행하기 - start()

 

현재는 두 Thread 모두 is_alive가 False로 나타나기 때문에 아무리 코드를 수행한다고 하더라도 1부터 20 사이의 짝수와 홀수가 출력되지 않는다. 이들 Thread를 실행하기 위해서는 Java와 동일하게 매서드로 이들 Thread가 동작할 수 있도록 제어해주어야 한다. 

 

Thread 인스턴스를 실행하는 매서드는 start(), run(), join()이 있는데, 단순히 개별 Thread를 실행하고자 할 때는 start() 매서드를 사용한다. 

 

 

Thread 적용 전에는 짝수 출력 후, 홀수를 출력했지만, 함수에 대해 개별 Thread를 생성하고 연달아 start() 매서드를 적용하니, 두 함수의 출력이 동시에 진행됨을 확인할 수 있다. 물론, 홀수를 출력하는 thread1을 먼저 start()로 진행했기 때문에 첫 출력은 항상 홀수가 나타난다.

 

이제 Thread 실행 코드 아래에 print() 문을 하나 추가해보려한다. 

 

 

 

start() 매서드 아래에 "Hello World"를 출력하는 print() 문을 하나 추가하자, 특이한 점이 하나 나타나는데, 모든 Thread의 실행이 종료되기도 전에 print("Hello World")가 실행된다는 것이다. 이는 코드가 작성된 test1.py의 __main__ 부분 역시 하나의 Thread로 동작하기 때문이다. 즉, Thead 선언으로 생성된 객체가 Stack Memory에 적재된 후, start()로 실행 될 때, 논리적으로 구분된 Memory로 이동을 하면서 3 구역의 메모리에서 각각의 함수를 동시에 처리하기 때문이다.

함수가 논리적으로 나뉘어진 뒤 동시에 실행되며, main은 thread1, 2의 종료를 기다리지 않는다

 

 

 

 

(3) Main Thread가 각 Thread의 종료를 기다리도록 설정하기 - join()

 

 그럼, Thread가 처리되기 전에는 Main 함수가 종료되지 않도록 할 수 있는 방법이 없을까? 굳이 기다리도록 만들 필요도 없지 않나라는 생각을 할 수도 있지만, 다음과 같이 print() 함수의 내용을 로그처럼 변경하고 각 함수에도 간단한 로그를 추가해보자.

 

 

 

Thread가 종료되기도 전에 프로세스가 종료된다는 로그가 출력된다. - 마치 부하직원한테 퇴근 전 일거리 던져주고 떠나가는 부장님 보는 느낌이다. 물론, 이런 프로그램도 없는 것은 아니지만, 현재 만든 프로그램은 모든 출력이 마무리되고 완전히 종료되는 것이 좋다는 것은 부장님(?) 빼고 다 안다.

 

start()로 개별 Thread를 실행하는 코드 아래에 각 thread의 join() 매서드를 추가로 작성해보자.

 

 

헤헤 못 가!

 

 

join() 매서드를 각 Thread 인스턴스에 적용하니 메인 프로세스가 종료된다는 로그가 정상적으로 출력된다. 이 매서드는 join() 매서드가 실행되는 Thread가 종료될 때까지 join() 코드 아래의 내용, 즉 메인 Thread(프로세스)의 수행을 일시 정지한다고 보면 된다. join()의 위치를 아래와 같이 변경해보자.

 

 

 

따라서 join() 매서드는 여러 Thread에 동시에 적용해야하는 경우, 코드 여기저기에 분산시키지 않고, 한 군데에서 코드를 작성한다.

 

실제 필자가 업무에 사용했던 코드의 Thread 관련 부분이다. join()이 마지막 부분에 집합되어 있다.

 

 

join() 매서드는 그 특성 상, 반드시 Thread의 실행이 진행되어야 사용할 수 있는 매서드다. 만약 Thread 실행 없이 바로 join() 매서드를 사용하면 Python Interpreter가 Thread 먼저 실행하고 join() 쓰라고 역정을 내는 것을 확인할 수 있다.

 

 

 

 

 

(4) 개별 Thread를 부모 Thread에서 실행 - run()

 

다시, 퇴근 직전 부하직원(Thread) 일 시키고 퇴근하는 부장님(Main Threa) 이야기, start()로 돌아가보자. 이번에 부장이 Thread1에게는 조금 쉬운 업무를, Thread2에게는 조금 어려운 업무를 주었다고 해보자. Thread1은 "부장님, 이거 저 혼자 끝내고 갈 수 있어요. 먼저 퇴근하셔도 됩니다!"라고 말하는데, Thread2는 "부장님, 퇴근 직전에 이렇게 어려운거 주시면 어떡합니까? 잘못되면 저 책임 못집니다. 저 가기 전까지 못가십니다"라고 말하며 부장의 퇴근을 막는다.

 

 

 

일반적인 경우라면, 부장 역시 자신의 업무 확인을 요청한 부하직원의 업무가 종료될 때까지 퇴근을 안한다. 위의 코드는 그러한 상황을 나타낸 것이라 보면 된다.

 

항상 해피엔딩만 만들면 인생이 아니지. 특이한 상황을 만들어보자. 부장이 Thread2 부하직원과 일하고 있는데, 부장의 집에서 급한 전화가 와, 부장이 퇴근을 해야하는 상황이다. 따라서 부장이 Thread2에게 "나 퇴근해야되니까, 너도 그냥 일 마무리하고 퇴근해!"라고 말하고 퇴근을 시도한다. 

 

여기서는 부장의 퇴근을 KeyboardInterrupt로 진행해보려한다.

 

 

 

일하는 도중에 Interrupt를 발생시켜 부장이 퇴근을 시도했으나, 결국 Thread2의 작업이 마무리되고 나서야 퇴근을 진행하는 것이 확인된다. 어떤 Thread가 Main Thread에 반드시 종속되어서 실행되어야하는 경우, 즉, 반드시 Main Thread가 살아있어야만 동작하는 Thread를 만들고자 한다면 지금까지 알아본 start(), join()으로는 구현이 불가능하다. 

 

다행히 부장님의 빠른 퇴근을 바랬던(?) 개발자들이 Thread에 run()이라는 매서드를 넣어둠으로써 이 기능을 구현해놓았다. run() 매서드는 Thread 인스턴스가 논리적으로 분할된 메모리에서 동작하도록 만드는 것이 아니라, 부모 프로세스에 기생하여 동작하도록 만든 프로세스라고 생각하면 된다. 그래서 부모 프로세스가 종료되면 run()으로 실행된 Thread 역시 함께 종료된다.

 

Thread1은 Main Thread와 독립적으로 동작하고 있음을 확인하자.

 

 

Thread를 run()으로 구동할 시 주의해야 할 점이 하나 있는데, 여러 Thread를 run()으로 구동시키면, 사실상 Thread를 사용하는 의미가 없게 된다는 것이다. 부장이 부하직원의 일을 하나 하나 확인해주어야하는데, 부장이 분신술을 쓸 수도 없으니 결국 run()을 호출한 순서대로 Thread를 처리해야하기 때문이다.

 

 

 


 

 

이미지 출처: https://www.stratascratch.com/blog/python-threading-like-a-pro/

 

 

하나의 Thread(Single Thread) 사용 시, 그리고 여러 Thread(Multi Thread)의 사용 시, 주 메모리에서 일어나는 일을 매우 이해하기 쉽게 그려놓은 이미지가 있어 공유한다. 

 

 

 

 

Fin.

반응형

댓글