반응형

Python에서 Thread/Process를 강제로 종료시키는 방법을 다루고 있습니다.



일반적으로 스레드나 프로세스를 갑자기 종료하는 것은 좋은 방법이 아닙니다.  왜냐하면 스레드/프로세스가 자원을 사용하던 중에 갑자기 종료되면 자원 해제 처리가 제대로 이루어질 수 없기 때문입니다.  스레드/프로세스를 강제 종료해도 자원 해제나 기타 다른 문제가 생길 가능성이 없다면 사용해볼 여지가 있습니다. 

 

본 포스트는 참고한 글을 기반으로 작성되었지만 대부분의 코드와 글이 원문과 차이가 있습니다.  원문에 있던 예외와 trace 관련 방법은 제외했습니다. 



중지 플래그 - 전역변수

 

중지 플래그 - 이벤트 객체

 

Using traces to kill threads :

 

스레드 대신 프로세스 사용

 

데몬(daemon)

 

참고



2024. 3. 17 최초작성




중지 플래그 - 전역변수 

 

스레드를 종료하기 위해 전역변수를 중지 플래그로 사용하는 예제 코드입니다.  스레드의 반복문에서 전역변수를 주기적으로 확인하여 값이 변경되면 스레드를 중지하도록 합니다. 아래 예제코드에서는 전역변수 stop_threads의 값이 True가되면 스레드가 중지됩니다.

 

import threading
import time


def run():

    # 무한 반복합니다.
    while True:
        print('스레드 실행중')

        # 전역변수 stop_threads의 값이 True이면 while문에서 빠져나와 run() 함수가 종료됩니다.       
        if stop_threads:
            break

# 전역변수로 stop_threads 함수를 선언합니다. 초기값은 False입니다. 
stop_threads = False

# run 함수를 스레드로 실행합니다.
t1 = threading.Thread(target = run)
t1.start()

# 1초 대기합니다.
time.sleep(1)

# 전역변수 stop_threads를 True로 바꾸어 쓰레드가 종료되도록 합니다.
stop_threads = True

# 스레드가 종료되길 대기합니다.
t1.join()

# 스레드가 종료된 것을 출력합니다.
print('스레드 종료됨')




실행하면 “스레드 실행중”이 반복적으로 출력되다가 “스레드 종료됨”이 출력된 후 프로그램이 종료됩니다.  

 

스레드 실행중

스레드 실행중

스레드 실행중

스레드 실행중

스레드 실행중

스레드 종료됨

(newenv) (base) webnautes@webnautesui-MacBookAir Python_Example % 




중지 플래그 - 이벤트 객체

 

전역변수를 사용하는 대신에 이벤트 객체를 스레드 실행시 넘겨주어 같은 방식으로 동작하도록 합니다. 차이가 있다면 전역변수의 값이 변경되는 것을 체크하는 대신 이벤트 객체가 설정되는 것을 체크합니다.  

 

import threading
import time


def run(stop_event):

    # 스레드가 무한반복합니다.
    while True:

        # 이벤트가 설정되어있다면 반복문을 중지하고 스레드를 종료합니다.
        if stop_event.is_set():
            break

        print('스레드 실행중')


# 전역 변수 대신에 이벤트 객체를 사용합니다.
stop_threads = threading.Event()

# 스레드 생성시 인수로 이벤트 객체를 넘겨줍니다.
t1 = threading.Thread(target=run, args=(stop_threads,))
t1.start()

# 1초 대기합니다.
time.sleep(1)

# 이벤트를 설정하여 쓰레드에게 중지 신호를 보냅니다.
stop_threads.set() 

# 스레드가 중지되기를 기다립니다.
t1.join()

# 스레드가 종료되었음을 출력합니다.
print('스레드 종료됨')




앞에서 본 예제와 동일한 방식으로 동작합니다. 실행하면 “스레드 실행중”이 반복적으로 출력되다가 “스레드 종료됨”이 출력된 후 프로그램이 종료됩니다.  

 

스레드 실행중

스레드 실행중

스레드 실행중

스레드 실행중

스레드 실행중

스레드 종료됨

(newenv) (base) webnautes@webnautesui-MacBookAir Python_Example % 





스레드 대신 프로세스 사용

threading 모듈을 사용하여 스레드를 생성하는 대신 multiprocessing 모듈을 사용하여 프로세스를 생성해봅니다.  두 모듈의 인터페이스가  유사하기 때문에 threading 모듈을 사용한 스레드 방식으로 구현된 코드를 multiprocessing 모듈을 사용한 프로세스 방식으로 쉽게 바꿀 수 있습니다. 



1부터 5까지의 수를 출력하는 3개의 스레드를 생성하는 코드입니다. 

 

import threading
import time


def func(number):
   
    end = 5
   
    for i in range(end+1):
        time.sleep(0.01)
        print(f'{number}번 스레드 : {i} / {end}')

# 3개의 스레드를 생성합니다.
for i in range(0, 3):
    thread = threading.Thread(target=func, args=(i,))
    thread.start()



3개의 스레드가 1부터 5까지의 수를 출력합니다. 아래 실행 결과에서는 3개의 스레드가 수를 출력하는 순서는 보장되지 않습니다. 실행할때마다 달라질 가능성이 있기 때문에 테스트시 항상 같은 순서로 출력되더라도 주의해야 합니다.

 

0번 스레드 : 0 / 5

1번 스레드 : 0 / 5

2번 스레드 : 0 / 5

0번 스레드 : 1 / 5

1번 스레드 : 1 / 5

2번 스레드 : 1 / 5

0번 스레드 : 2 / 5

1번 스레드 : 2 / 5

2번 스레드 : 2 / 5

1번 스레드 : 3 / 5

2번 스레드 : 3 / 5

0번 스레드 : 3 / 5

1번 스레드 : 4 / 5

2번 스레드 : 4 / 5

0번 스레드 : 4 / 5

1번 스레드 : 5 / 5

2번 스레드 : 5 / 5

0번 스레드 : 5 / 5




이제 위 코드를 프로세스를 사용하는 코드로 바꾸어 봅니다. multiprocessing 모듈로 바꾸로 스레드 대신 프로세스를 생성하도록 변경하면 거의 비슷한 방식으로 구현할 수 있습니다.  주의할 점은 운영체제에 따라선 아래 코드를 추가하지 않으면 에러가 날 수 있습니다.

 

if __name__ == '__main__':

 

import multiprocessing
import time


def func(number):
   
    end = 5
   
    for i in range(end+1):
        time.sleep(0.01)
        print(f'{number}번 프로세스 : {i} / {end}')


if __name__ == '__main__':
    # 3개의 프로세스를 생성합니다.
    for i in range(0, 3):
        process = multiprocessing.Process(target=func, args=(i,))
        process.start()



실행해보면 3개의 프로세스가 1에서 5까지의 수를 출력합니다. 스레드의 경우와 마찬가지로 프로세스도 3개의 프로세스가 수를 출력하는 순서가 보장되지 않습니다.

 

0번 프로세스 : 0 / 5

1번 프로세스 : 0 / 5

2번 프로세스 : 0 / 5

1번 프로세스 : 1 / 5

0번 프로세스 : 1 / 5

2번 프로세스 : 1 / 5

0번 프로세스 : 2 / 5

2번 프로세스 : 2 / 5

1번 프로세스 : 2 / 5

2번 프로세스 : 3 / 5

1번 프로세스 : 3 / 5

0번 프로세스 : 3 / 5

0번 프로세스 : 4 / 5

1번 프로세스 : 4 / 5

2번 프로세스 : 4 / 5

2번 프로세스 : 5 / 5

0번 프로세스 : 5 / 5

1번 프로세스 : 5 / 5




threading 모듈과 multiprocessing 모듈의 인터페이스가 유사하지만 각각 스레드와 프로세스를 생성하기 때문에 구현 방식이 다릅니다. 스레드 간에는 전역 변수를 공유하는 반면 프로세스는 각각 분리된 공간을 가지고 있기 때문에 변수를 공유하지 않습니다.  

 

스레드를 강제로 종료하면 전역 변수를 공유하는 다른 스레드에 영향을 줄 수 있는 반면 프로세스는 변수를 공유하지 않아서 프로세스를 강제로 종료해도 다른 프로세스에 영향을 주지 않기 때문에 스레드를 강제로 종료시키는 것보단 프로세스를 강제로 종료시키는 것이 안전합니다. 



프로세스 생성시 사용되는 multiprocessing 모듈의 Process 클래스에는 프로세스를 종료하는 terminate() 메서드가 제공됩니다.  2초 후 모든 프로세스를 종료시키도록 코드를 수정해봅니다. 

 

import multiprocessing
import time


def func(number):
   
    end = 5
   
    for i in range(end+1):
        time.sleep(0.01)
        print(f'{number}번 프로세스 : {i} / {end}')

    print(f'{number} 스레드가 종료되었습니다.')


if __name__ == '__main__':

    # 프로세스 객체를 리스트에 담아 나중에 한번에 종료시킬 때 사용합니다.
    all_processes = []

    # 3개의 스레드를 생성합니다.
    for i in range(0, 3):
        process = multiprocessing.Process(target=func, args=(i,))
        process.start()

        all_processes.append(process)


    # 2초후 모든 프로세스를 종료합니다. 
    time.sleep(0.05)

    for process in all_processes:
        process.terminate()

    print('모든 프로세스가 종료되었습니다.')




실행해보면 3개의 프로세스가 강제로 종료되기때문에 종료되었다는 메시지가 출력되지는 않습니다. 자원 해제 부분은 스레드와 마찬가지로 주의해야 합니다. 

 

0번 프로세스 : 0 / 5

1번 프로세스 : 0 / 5

2번 프로세스 : 0 / 5

0번 프로세스 : 1 / 5

1번 프로세스 : 1 / 5

2번 프로세스 : 1 / 5

모든 프로세스가 종료되었습니다.

(newenv) (base) webnautes@webnautesui-MacBookAir Python_Example % 




데몬(daemon) 

데몬 스레드는 메인 프로그램이 종료될 때 같이 종료되는 스레드를 의미합니다. 



파이썬에서 기본적으로 스레드는 비데몬 스레드이기 때문에 메인 프로그램이 종료되도 같이 종료되지 않습니다. 메인 프로그램이 종료하려고 할때 비데몬 스레드가 실행중이면 메인 프로그램은 비데몬 스레드가 종료되기를 기다리게 되기 때문에 메인 프로그램이 종료되지 않습니다.  다음 예제를 통해 확인해보세요.

 

import threading
import time
import sys


def func():

    # count = 0
   
    # 스레드내에서 무한 반복되기 때문에 스레드가 종료되지 않습니다.
    print('스레드내에서 무한반복해서 종료가 되지 않습니다')
    while True:
       
        time.sleep(0.5)
       
    print('스레드가 종료됩니다.')


# 스레드를 시작합니다.
print('스레드를 시작합니다.')
t1 = threading.Thread(target=func)
t1.start()

# 2초 대기합니다.
time.sleep(2)

print('메인 프로그램을 종료려고 합니다.')
sys.exit()




실행결과 스레드가 실행중이라 메인 프로그램에서 sys.exit()를 호출했음에도 메인 프로그램이 종료되지 않고 대기 상태가 됩니다. 

 

스레드를 시작합니다.

스레드내에서 무한반복해서 종료가 되지 않습니다

메인 프로그램을 종료려고 합니다.




이번엔 데몬 스레드로 바꿔봅니다.  데몬 스레드는 메인 프로그램이 종료되는 즉시 같이 종료됩니다.  메인 프로그램 종료와 동시에 모든 스레드를 같이 종료시키려고 하는 경우 유용합니다.  스레드를 데몬 스레드로 변경하는 방법은 간단합니다. daemon 인수에 True를 대입하면 됩니다. 앞의 코드를 수정해봅니다. 다음 코드만 추가되었습니다.

 

t1.daemon = True

 

import threading
import time
import sys


def func():

    # count = 0
   
    # 스레드내에서 무한 반복되기 때문에 스레드가 종료되지 않습니다.
    print('스레드내에서 무한반복해서 종료가 되지 않습니다')
    while True:
       
        time.sleep(0.5)
       
    print('스레드가 종료됩니다.')


# 스레드를 시작합니다.
print('스레드를 시작합니다.')
t1 = threading.Thread(target=func)

# 데몬 스레드로 설정합니다.
t1.daemon = True

t1.start()



# 2초 대기합니다.
time.sleep(2)

print('메인 프로그램을 종료려고 합니다.')
sys.exit()




실행해보면 메인 프로그램이 종료되는 순간 스레드도 같이 종료된 것을 볼 수 있습니다.  중요한 점은 스레드가 갑자기 종료된 거라서 스레드 마지막에 있는  '스레드가 종료됩니다.' 메시지가 출력되지 않았습니다. 

 

스레드를 시작합니다.

스레드내에서 무한반복해서 종료가 되지 않습니다

메인 프로그램을 종료려고 합니다.

(newenv) (base) webnautes@webnautesui-MacBookAir Python_Example % 



스레드가 갑자기 종료되었기 때문에 사용중인 자원을 해제하지 않을 가능성이 있습니다. 강제로 스레드를 종료해도 자원해제 같은 문제가 발생하지 않는 경우에만 사용하는 것을 권장합니다. 




참고

 

https://www.geeksforgeeks.org/python-different-ways-to-kill-a-thread/ 

 

반응형

문제 발생시 지나치지 마시고 댓글 남겨주시면 가능한 빨리 답장드립니다.

도움이 되셨다면 토스아이디로 후원해주세요.
https://toss.me/momo2024


제가 쓴 책도 한번 검토해보세요 ^^

+ Recent posts