본문 바로가기

파이선 (python)

[Python] 스레드(Thread)를 구현해보자

728x90

[ 스레드 기본 ] 

* 파이썬에서 제공되는 threading 모듈을 사용하여 스레드를 구현해 보자

* 각기 다른 기능을 하는 함수 2개를 만들고 스레드로 동시에 실행하는 코드를 작성한다.

- function_1 : 숫자 0부터 9까지 순차적으로 출력하는 함수 

- function_2 : 문자 A부터 J까지 순차적으로 출력하는 함수

import threading

# 0 부터 9 까지 순차적으로 출력하는 함
def function_1():   
    for i in range(10):
        print(i)

# A 부터 J 까지 순차적으로 출력하는 함수
def function_2():
    for letter in 'ABCDEFGHIJ':
        print(letter)

# 실행할 함수를 thread1 과 thread2로 각각 설정
thread1 = threading.Thread(target=function_1) 
thread2 = threading.Thread(target=function_2)

# 스레드를 시작한다.
thread1.start() 
thread2.start()

# 스레드가 끝날 때까지 기다린다.
thread1.join()  
thread2.join()

* 위 코드를 test_threading_01.py로 저장한 후 실행하면 아래와 같다.

* 스레드 1과 스레드 2가 동시에 실행되기 때문에 숫자와 문자가 출력되는 순서는 실행할 때마다 다르게 나타난다.

 

[ 스레드에서 글로벌 변수 사용 ]

* 각각의 스레드에서 사용되는 변수가 글로벌 변수이면 어떻게 되는지 살펴보자

* 동일한 기능을 하는 함수 2개를 만들고 스레드로 동시에 실행하는 코드를 작성한다.

- function_1 : 글로별 변수 count에 0부터 1000000까지 순차적으로 더하는 함수 

- function_2 : 글로별 변수 count에 0부터 1000000까지 순차적으로 더하는 함수

import threading

# 전역 변수를 선언하고 0으로 Set
count = 0

# 0 부터 1000000 까지 1씩 증가시키는 함수1
def function_1():
    global count
    for _ in range(1000000):
        count += 1

# 0 부터 1000000 까지 1씩 증가시키는 함수2
def function_2():
    global count
    for _ in range(1000000):
        count += 1

# 실행할 함수를 thread1 과 thread2로 각각 설정
thread1 = threading.Thread(target=function_1) 
thread2 = threading.Thread(target=function_2)

# 스레드를 시작한다.
thread1.start() 
thread2.start()

# 스레드가 끝날 때까지 기다린다.
thread1.join()  
thread2.join()

# 연산된 count 값을 출력한다.
print(f"Count = {count}")

* 위 코드를 test_threading_02.py로 저장한 후 실행하면 아래와 같다.

 

[ 하나의 스레드가 자원을 독점하는 방법 ]

* GIL(Gloabl Interpreter Lock)은 하나의 스레드가 자원을 독점하는 형태로 실행된다.

* 멀티스레드일 경우, 여러개의 스레드가 병렬적으로 일하는 것이 아니고 여러개의 스레드 중 하나의 스레드만 실행될 수 있도록 하는 상호 배제 잠금(mutex)을 한다.

* 하나의 스레드가 모든 자원을 가지면 다른 스레드에게 lock을 걸어 다른 스레드는 작업을 못하게 할 수 있다.

* 위의 소스 코드에서 글로벌 변수 값을 2개의 스레드가 동시에 증가시킬 때 결과가 2000000으로 나오긴 했지만 복잡한 연산을 하거나 처리 과정이 복잡할 경우 결과가 엉뚱하게 나올 수 있다.

* 그 이유는 스레드 세이프(thread-safe) 때문인데 하나의 쓰레드가 count값을 변경하는 과정에 다른 쓰레드가 count값을  변경할 수 있기 때문이다.

이렇듯 여러 쓰레드가 하나의 공유 자원이 동시에 접근하면서 발생하는 문제를 race condition(경쟁 상태)이라 하며 이 문제를 해결하기 위해 mutex 등을 도입한다.
* Mutex는 이렇게 공유 자원에 하나의 쓰레드만 진입하며 작업을 처리할 수 있도록 만들어진 lock 개념이다.

* 위 소스 코드에서 count 값을 변경하기 직전에 lock.acquire()을 호출해 주고 변경작업이 완료되면 lock.release()을 호출해주면 된다.

import threading

# 스레드 Lock 기능을 설정
lock = threading.Lock()

# 전역 변수를 선언하고 0으로 Set
count = 0

# 0 부터 1000000 까지 1씩 증가시키는 함수1
def function_1():
    lock.acquire()  # 스레드 Lock을 활성화 시킨다.
    global count
    for _ in range(1000000):
        count += 1
    lock.release()  # 스레드 Lock을 해제 시킨다.

# 0 부터 1000000 까지 1씩 증가시키는 함수2
def function_2():
    lock.acquire()
    global count
    for _ in range(1000000):
        count += 1
    lock.release()

# 실행할 함수를 thread1 과 thread2로 각각 설정
thread1 = threading.Thread(target=function_1) 
thread2 = threading.Thread(target=function_2)

# 스레드를 시작한다.
thread1.start() 
thread2.start()

# 스레드가 끝날 때까지 기다린다.
thread1.join()  
thread2.join()

# 연산된 count 값을 출력한다.
print(f"Count in Lock = {count}")

* 위 코드를 test_threading_03.py로 저장한 후 실행하면 아래와 같다.

* 참고로... 현재 내 PC에서는 lock 개념을 추가하지 않아도 count변수가 달라지는 상황은 발생하지 않는다.

* 내 PC가 빨라서 그런지? 아니면 파이썬 threading 라이브러리가 업그레이드 되어서 전역변수를 사용하게 되면 알아서 lock 기능이 적용되는지 정확히 알 수 없다. 

* 일단, 2개 이상의 스레드가 동시에 실행될 때 필요 시 lock 개념을 도입한다는 개념만 알고 넘어가자.