Computer >> 컴퓨터 >  >> 프로그래밍 >> Python

Python에서 우선순위 큐(Priority Queue) 구현하는 방법 완벽 가이드

들어가며: queue 모듈이란?

Python의 queue 모듈은 멀티스레드 프로그래밍에 적합한 선입선출(FIFO), 후입선출(LIFO) 자료구조를 제공합니다. 큐는 생성자(producer) 스레드와 소비자(consumer) 스레드 사이에서 세션 정보, 경로, 변수 등 다양한 데이터를 안전하게 전달하는 데 활용될 수 있습니다. 또한 락(lock) 처리는 호출자를 위해 내부적으로 자동으로 수행되므로, 개발자가 직접 동기화를 관리할 필요가 없습니다.

참고: 이 글은 독자가 큐의 기본 개념을 이미 이해하고 있다고 가정하고 진행됩니다. 큐에 대한 개념이 부족하다면 관련 자료를 먼저 살펴보시길 권장합니다.

1. 기본 FIFO 큐 구현하기

먼저 가장 기본적인 선입선출(FIFO) 큐를 구현해 보겠습니다.

import queue
fifo = queue.Queue()

# 큐에 숫자 넣기
for i in range(5):
fifo.put(i)

# 큐가 비어 있지 않으면 숫자 꺼내기
print(f"Output \n")
while not fifo.empty():
print(f" {fifo.get()} ")

실행 결과

0
1
2
3
4

위 예제는 단일 스레드를 사용하여, 요소들이 삽입된 순서와 동일한 순서로 큐에서 제거되는 것을 보여줍니다. 먼저 넣은 값이 먼저 나오는 것이 바로 FIFO의 핵심 특징입니다.

2. 기본 LIFO 큐 구현하기

이번에는 후입선출(LIFO) 방식의 큐를 구현해 보겠습니다.

import queue
lifo = queue.LifoQueue()

# 큐에 숫자 넣기
for i in range(5):
lifo.put(i)

print(f"Output \n")
# 큐가 비어 있지 않으면 숫자 꺼내기
while not lifo.empty():
print(f" {lifo.get()} ")

실행 결과

4
3
2
1
0

위 예제에서 확인할 수 있듯이, LIFO 큐에서는 가장 최근에 넣은 요소가 get() 메서드에 의해 먼저 제거됩니다. 스택(stack)과 동일한 동작 방식이라고 생각하면 이해하기 쉽습니다.

3. 우선순위 큐(Priority Queue) 구현하기

마지막으로 이 글의 핵심인 우선순위 큐를 살펴보겠습니다.

때로는 큐에 있는 항목들의 처리 순서가 단순히 생성 순서나 추가 순서가 아니라, 각 항목의 우선순위(priority)에 따라 결정되어야 할 때가 있습니다. 예를 들어, 운영 환경에서 실행 중인 비즈니스 핵심 작업이 CPU를 가장 많이 필요로 하며, 개발자가 인쇄하려는 단순 문서 출력 작업보다 우선권을 가져야 하는 경우를 생각해 볼 수 있습니다.

PriorityQueue는 큐 내용물의 정렬 순서(sort order)를 기준으로 어떤 항목을 먼저 가져올지 결정합니다. 즉, 우선순위 값이 낮을수록 먼저 처리됩니다.

import queue
import threading

# 우선순위와 설명을 받아 우선순위를 검증하는 클래스
class Job:
def __init__(self, priority, description):
self.priority = priority
self.description = description
print('New job:', description)
return

def __eq__(self, other):
try:
return self.priority == other.priority
except AttributeError:
return NotImplemented

def __lt__(self, other):
try:
return self.priority < other.priority
except AttributeError:
return NotImplemented

# 우선순위 큐 생성 및 우선순위 정의
q = queue.PriorityQueue()
q.put(Job(90, 'Developer-Print job'))
q.put(Job(2, 'Business-Report job'))
q.put(Job(1, 'Business-Critical Job'))

# 작업 처리 함수
def process_job(q):
while True:
next_job = q.get()
print(f" *** Now, Processing the job - {next_job.description}")
q.task_done()

# 워커 스레드 정의
workers = [
threading.Thread(target=process_job, args=(q,)),
threading.Thread(target=process_job, args=(q,))]

# 스레드 시작 및 종료 대기
for w in workers:
w.setDaemon(True)
w.start()

q.join()

작업 등록 결과

New job: Developer-Print job
New job: Business-Report job
New job: Business-Critical Job

작업 처리 결과

*** Now, Processing the job - Business-Critical Job
*** Now, Processing the job - Business-Report job
*** Now, Processing the job - Developer-Print job

핵심 포인트 정리

이 예제는 여러 스레드가 작업을 소비하면서, get()이 호출된 시점에 큐에 있는 항목들의 우선순위를 기준으로 처리가 이루어지는 모습을 보여줍니다. 주목할 점은 작업이 추가된 순서와 무관하게, 비즈니스 중요도(criticality)에 따라 처리 순서가 결정된다는 것입니다. 우선순위가 1인 'Business-Critical Job'이 가장 먼저, 그다음으로 2인 'Business-Report job', 마지막으로 90인 'Developer-Print job'이 처리되었습니다.

이처럼 queue.PriorityQueue를 활용하면 멀티스레드 환경에서도 안전하게 우선순위 기반 작업 스케줄링을 구현할 수 있습니다. 커스텀 객체를 사용할 때는 위 예제처럼 __lt__(미만 연산자) 같은 비교 메서드를 반드시 정의해야 한다는 점도 기억해 두세요.