실의 의미를 가지고 있는 thread
는 프로그램의 실행흐름 단위입니다. 하나의 프로세스 안에서 여러개의 thread를 만들 수 있고, 한 프로세스에 자원이 부여되면 그 안에서 프로세스에 속한 여러 쓰레드가 자원을 공유할 수 있습니다. 이를 multithread
라고 부릅니다.
쓰레드는 동시성을 위해서 만들어진 개념입니다. 지금까지 작성한 프로그램들은 최소 하나의 프로세스단위와 하나의 쓰레드를 가지고 있는데요! 하나의 프로세스 안에서 두개 이상의 쓰레드를 만들게 되면 두개 이상의 쓰레드가 동시에 작업을 합니다.
단순 반복하는 작업을 분리해서 처리할 수 있으며 1. CPU 사용률을 향상하고 2. 효율적인 자원 활용 및 응답성 향상 3. 간결한 코드 및 유지 보수성을 향상할 수 있다는 장점이 있습니다.
다음의 코드는 일반적으로 지금까지 작성된 프로그램을 시간 측정한 것입니다.
import time
if __name__ == "__main__":
increased_num = 0
start_time = time.time()
for i in range(100000000):
increased_num += 1
print("--- %s seconds ---" % (time.time() - start_time))
print("increased_num=",end=""), print(increased_num)
print("end of main")
위 프로그램은 단일 프로세스의 단일 쓰레드로 일억번을 +=1하는 프로그램으로, 12초 가량이 걸렸고, 증가된 숫자와 결과 모두 일억번인 것을 확인할 수 있습니다.
아래의 코드는 쓰레드를 두개로 작성한 프로그램입니다.
import threading
import time
shared_number = 0
def thread_1(number):
global shared_number
print("number = ",end=""), print(number)
for i in range(number):
shared_number += 1
def thread_2(number):
global shared_number
print("number = ",end=""), print(number)
for i in range(number):
shared_number += 1
if __name__ == "__main__":
threads = [ ]
start_time = time.time()
t1 = threading.Thread( target= thread_1, args=(50000000,) )
t1.start()
threads.append(t1)
t2 = threading.Thread( target= thread_2, args=(50000000,) )
t2.start()
threads.append(t2)
for t in threads:
t.join()
print("--- %s seconds ---" % (time.time() - start_time))
print("shared_number=",end=""), print(shared_number)
print("end of main")
파이썬에서 멀티 쓰레드를 사용하기 위해 threading 모듈을 import해주었습니다. 그리고 thread 클래스에 있는 target인자에 쓰레드 함수를 자정하고, args로 매개변수를 전달받았습니다.
두개의 쓰레드로 실행한 결과 숫자는 1억이 되지 않았고, 계산 속도도 반으로 줄어들지 않았습니다. 왜 그런걸까요?
이를 알기 위해서는 GIL
, Mutex
, Condition Variable
을 알아야 합니다.
대부분의 프로그래밍 언어는 스레드를 실행하는 환경에서 자원을 보호하기 위해 Lock
합니다. Lock
은 Mutual exclution
즉 Mutex
를 강제하기 위해 설계되었습니다.
파이썬은 Lock
으로 GIL
정책을 사용합니다. GIL은 Global Interpreter Lock으로 파이썬은 한번에 하나의 쓰레드가 자원을 활용하기 위해서(코드를 실행하기 위해서), 하나의 프로세스 안에 모든 자원에 Lock
을 Global하게 관리합니다.
파이썬의 GIL정책 때문에, shared_number를 공유하는 두 쓰레드가 동시에 실행되지 않고 한번에 하나의 쓰레드만 계산을 실행하기 때문에 실행 시간이 비슷합니다.
Mutex
는 상호 배제의 의미로 주어진 특정 시간에 하나의 쓰레드만 특정 자원을 사용할 수 있는 것을 말합니다. 한 프로그램에 여러개의 스레드가 존재한다면 Mutex는 스레드가 특정 자원(특정 코드)를 동시에 사용하는 것을 제한하며 다른 스레드를 'Lock'하고 작업 영역으로의 진입을 제한합니다.
작업 영역은 프로세스 간에 공유 자원을 접근하는데 있어서 문제가 발생하지 않도록 동시에 접근하는 것을 막고, 한번에 하나의 프로세스를 이용하게 하며 더 정확한 표현으로는임계 영역
입니다.
lock()함수를 통해 첫 번째 쓰레드가 작업을 할 때 두 번째 쓰레드는 잠기고 이 작업이 끝나면 잠금을 해제한 후 반대의 상황으로 잠그게 합니다. 잠금과 해제는 acquire()
과 release()
매서드를 통해 구현할 수 있습니다.
Condition Variable는 조건변수로 Mutex로 처리하기 힘든 문제를 해결합니다. 이를 활용하면 thread_1는 동작을 멈추고 thread_2가 호출할 때만 동작하게 되므로 효율적으로 자원을 사용할 수 있습니다.
아래의 코드는 상호배제와 관련된 매서드를 통해 'shared_number'에서 두 스레드를 통해 각각 오천만씩 증가시켜 1까지 증가시키는 코드입니다.
import threading
import time
shared_number = 0
def thread_1(number):
global shared_number
mutex.acquire() # acquire()를 통해 접근 권한 획득
print("number = ",end=""), print(number)
for i in range(number):
shared_number += 1
mutex.release() # release()를 통해 접근 권한 반환
def thread_2(number):
global shared_number
mutex.acquire() # acquire()를 통해 접근 권한 획득
print("number = ",end=""), print(number)
for i in range(number):
shared_number += 1
mutex.release() # release()를 통해 접근 권한 반환
if __name__ == "__main__":
mutex = threading.Lock() # thread 모듈의 lock 함수 선언을 통해 상호 배제 구현
threads = [ ]
start_time = time.time()
t1 = threading.Thread( target= thread_1, args=(5000000,) )
t1.start()
threads.append(t1)
t2 = threading.Thread( target= thread_2, args=(5000000,) )
t2.start()
threads.append(t2)
for t in threads:
t.join()
print("--- %s seconds ---" % (time.time() - start_time))
print("shared_number=",end=""), print(shared_number)
print("end of main")
acquire()
, release()
메서드로 접근 권환을 획득 및 반환을 하고 mutex = threading.Lock()
메서드를 통해 thread모듈에서 lock 함수의 선언을 통해 상호 배제를 구현하였습니다. 그 후 join()
메서드를 통해 두 스레드의 결과값을 합친 값이 1억으로 출력되도록 합니다.