소개
이 글에서는 실행해야 할 모든 작업을 담아두는 작업 대기열(큐)과, 이 큐와 상호작용하며 요소를 하나씩 꺼내 처리하는 스레드 풀을 만들어 보겠습니다.
먼저 "큐(Queue)란 무엇일까?"라는 질문부터 시작해 보겠습니다. 큐는 서로 다른 요소들을 아주 특정한 순서로 관리하는 자료구조입니다. 실생활의 예를 통해 쉽게 이해해 보겠습니다.
마트 계산대 앞에 줄을 서서 계산을 기다리고 있다고 상상해 보세요. 계산을 기다리는 사람들의 줄에서는 다음과 같은 규칙이 지켜집니다.
- 사람들은 줄의 한쪽 끝에서 들어와 반대쪽 끝에서 나갑니다.
- A가 B보다 먼저 줄에 섰다면, A가 B보다 먼저 줄을 빠져나갑니다(B가 연예인이거나 우선순위가 더 높은 경우는 예외입니다).
- 모든 사람이 계산을 마치면 줄에는 아무도 남지 않습니다.
프로그래밍에서의 큐도 똑같은 방식으로 동작합니다.
- enqueue(삽입) – 큐의 맨 뒤에 새로운 요소를 추가합니다.
- dequeue(삭제) – 큐의 맨 앞에서 요소를 꺼내 제거합니다.
여기에 더해 알아두면 좋은 두 가지 개념이 있습니다.
- FIFO(First In First Out, 선입선출) – 가장 먼저 넣은 요소가 가장 먼저 나옵니다.
- LIFO(Last In First Out, 후입선출) – 가장 마지막에 넣은 요소가 가장 먼저 나옵니다.
파이썬에서 큐 자료구조를 구현하는 방법
파이썬의 queue 모듈은 큐 자료구조를 손쉽게 사용할 수 있도록 도와줍니다. 각 큐 객체는 다음과 같은 메서드를 제공합니다.
- get() – 큐에서 다음 요소를 반환합니다.
- put() – 큐에 새로운 요소를 추가합니다.
- qsize() – 현재 큐에 들어 있는 요소의 개수를 반환합니다.
- empty() – 큐가 비어 있는지 여부를 불리언 값으로 반환합니다.
- full() – 큐가 가득 찼는지 여부를 불리언 값으로 반환합니다.
그럼 지금부터 단계별로 멀티스레드 큐를 구현해 보겠습니다.
1단계: 곱셈 결과를 출력하는 함수 작성
인자 x를 받아 1부터 x까지의 숫자를 차례대로 곱한 값을 리스트로 반환하는 함수를 만듭니다. 예를 들어 5를 전달하면 1×5, 2×5, 3×5, 4×5, 5×5를 계산하여 [5, 10, 15, 20, 25]라는 결과를 돌려줍니다.
def print_multiply(x):
output_value = []
for i in range(1, x + 1):
output_value.append(i * x)
print(f"*** {x}의 곱셈 결과 - {output_value}")
print_multiply(5)
실행 결과:
*** 5의 곱셈 결과 - [5, 10, 15, 20, 25]
2단계: 큐를 처리하는 함수 작성
다음으로 process_queue() 함수를 작성하여 큐 객체에서 다음 요소를 가져오도록 합니다. 로직은 아주 간단합니다. 큐가 빌 때까지 요소를 계속 꺼내 처리하고, time.sleep()으로 진행 속도를 살짝 늦춥니다.
def process_queue():
while True:
try:
value = my_queue.get(block=False)
except queue.Empty:
return
else:
print_multiply(value)
time.sleep(2)
3단계: 스레드 클래스 정의
새 인스턴스가 생성되어 시작될 때 process_queue() 함수를 호출하는 클래스를 만듭니다.
class MultiThread(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name
def run(self):
print(f"** 스레드 시작 - {self.name}")
process_queue()
print(f"** 스레드 완료 - {self.name}")
4단계: 입력값으로 큐 채우기
이제 숫자 리스트를 입력받아 큐를 채웁니다.
# 변수 설정
input_values = [5, 10, 15, 20]
# 큐 채우기
my_queue = queue.Queue()
for x in input_values:
my_queue.put(x)
5단계: 전체 코드 통합
지금까지 만든 모든 조각을 하나로 합쳐 보겠습니다.
import queue
import threading
import time
# 스레드 클래스
class MultiThread(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.name = name
def run(self):
print(f"** 스레드 시작 - {self.name}")
process_queue()
print(f"** 스레드 완료 - {self.name}")
# 큐 처리 함수
def process_queue():
while True:
try:
value = my_queue.get(block=False)
except queue.Empty:
return
else:
print_multiply(value)
time.sleep(2)
# 곱셈 함수
def print_multiply(x):
output_value = []
for i in range(1, x + 1):
output_value.append(i * x)
print(f"*** {x}의 곱셈 결과 - {output_value}")
# 입력 변수
input_values = [2, 4, 6, 5, 10, 3]
# 큐 채우기
my_queue = queue.Queue()
for x in input_values:
my_queue.put(x)
# 스레드 4개 초기화 및 생성
thread1 = MultiThread('First')
thread2 = MultiThread('Second')
thread3 = MultiThread('Third')
thread4 = MultiThread('Fourth')
# 스레드 시작
thread1.start()
thread2.start()
thread3.start()
thread4.start()
# 스레드 종료 대기
thread1.join()
thread2.join()
thread3.join()
thread4.join()
실행 결과
** 스레드 시작 - First *** 2의 곱셈 결과 - [2, 4] ** 스레드 시작 - Second *** 4의 곱셈 결과 - [4, 8, 12, 16] ** 스레드 시작 - Third *** 6의 곱셈 결과 - [6, 12, 18, 24, 30, 36] ** 스레드 시작 - Fourth *** 5의 곱셈 결과 - [5, 10, 15, 20, 25] *** 10의 곱셈 결과 - [10, 20, 30, 40, 50, 60, 70, 80, 90, 100] *** 3의 곱셈 결과 - [3, 6, 9] ** 스레드 완료 - Third ** 스레드 완료 - Fourth ** 스레드 완료 - Second ** 스레드 완료 - First
마무리: 큐의 동작 원리 이해하기
이렇게 성공적으로 큐 개념을 구현했습니다. 스레드는 4개인데 처리해야 할 값은 6개입니다. 따라서 큐에 먼저 도착한 값부터 실행되고, 나머지 값들은 다른 작업이 끝나기를 기다리며 줄을 서게 됩니다.
이는 실생활과 매우 유사합니다. 계산대가 3개인데 계산을 기다리는 사람이 10명이라면, 10명은 3개의 줄에 나눠 서게 되고, 계산을 마친 사람은 줄을 빠져나가 다음 사람에게 자리를 내어주는 것과 같습니다.