프로세스(process)는 컴퓨터에서 연속적으로 실행되고 있는 프로그램이다. 스레드(thread)는 어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위를 말한다. 한 프로그램 혹은 프로세스는 하나의 스레드를 가지고 있으며, 둘 이상의 스레드를 동시에 가지고 실행할 수도 있다. 이러한 실행 방식을 멀티스레드라고 한다.
같은 프로세스 내의 스레드는 Heap, Data, Code 영역의 가상메모리를 공유한다. Stack, Program Counter(PC), Registers 영역의 메모리는 각각의 스레드마다 할당된다.
파이썬에서는 하나의 프로세스에서 하나의 스레드만 실행할 수 있도록 락(Lock)을 걸어 관리한다. GIL로 GC(Garbage Collertor)의 구현이 용이해졌지만, 스레드가 병렬적으로 실행되지않아 단일 스레드와 멀티 스레드의 성능이 큰 차이가 없다.
하지만 GIL은 CPU bounding에서 사용되고, I/O bounding은 영향을 받지 않아 I/O bounding작업에서는 Multi-Threading이 가능하다.
Qt의 Thread를 추상화한 클래스로 QThread 인스턴스는 자체 스레드를 생성한다.
애니메이션 파일(APNG)를 GUI 화면에 무한 재생하는 예제.
pyside6에서는 QMovie를 지원해 애니메이션 파일을 재생할 수 있다. 하지만 사용할 수 없는 환경에서는 수동으로 프레임을 로드하면서, 주 스레드를 차단하지 않아야 하기 때문에 별도의 스레드에서 처리한다.
import imageio.v3 as iio
from PySide6.QtCore import QThread, Signal
from PySide6.QtGui import QImage, QPixmap
class AnimationThread(QThread):
frame_ready = Signal(QPixmap)
def __init__(self, path):
super().__init__()
self.running = True
self.path = path
self.frames = []
def run(self):
self.running = True
self.frames = self.load_frames(self.path)
self.total_frames = len(self.frames)
frame_index = 0
while self.running and self.total_frames > 0:
frame = self.frames[frame_index]
self.frame_ready.emit(frame) # UI 업데이트
frame_index = (frame_index + 1) % self.total_frames
self.msleep(100) # FPS
def load_frames(self, path):
# APNG 파일 로드
try:
frames = []
img_frames = iio.imread(path, plugin="pillow", index=None) # pillow 투명도 유지
if img_frames is None:
raise ValueError("Unable to invoke APNG frame.")
for frame in img_frames:
h, w, ch = frame.shape
bytes_per_line = ch * w
qimg = QImage(frame.data, w, h, bytes_per_line, QImage.Format.Format_RGBA8888)
pixmap = QPixmap.fromImage(qimg)
frames.append(pixmap)
except Exception as e:
print(f"{path} loading error: {e}")
frames = []
return frames
def stop(self):
try:
if self.isRunning():
self.running = False
self.quit()
self.wait()
except Exception as e:
print(f"APNG Thread safe stop Error: {e}")
import sys
from PySide6.QtWidgets import (
QApplication, QMainWindow, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem
)
from mainwindow import Ui_MainWindow
from anim_thread import AnimationThread
class mainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super(mainWindow, self).__init__()
self.setupUi(self)
self.graphicsView = QGraphicsView(self.centralwidget)
self.scene = QGraphicsScene(self)
self.graphicsView.setScene(self.scene)
self.graphics_item = QGraphicsPixmapItem()
self.scene.addItem(self.graphics_item)
# animation 스레드 설정
self.apng_thread = AnimationThread(path="file_path")
self.apng_thread.frame_ready.connect(self.update_frame)
self.apng_thread.start()
def update_frame(self, pixmap):
self.graphics_item.setPixmap(pixmap)
self.scene.setSceneRect(0, 0, pixmap.width(), pixmap.height())
def closeEvent(self, event):
self.apng_thread.stop()
event.accept()
if __name__ == '__main__':
app = QApplication(sys.argv)
Window = mainWindow()
Window.show()
sys.exit(app.exec())
closeEvent(): gui 창의 x(close) 버튼을 클릭하면 호출되는 이벤트run(): 스레드가 시작될 때 호출되는 메서드. 스레드 작업 코드를 작성.start(): 스레드의 실행 시작 메서드. run()을 호출하여 작업수행 로직을 실행quit(): 스레드의 이벤트 루프 종료 요청 메서드.wait(): 스레드의 작업이 종료될 때까지 기다리는 메서드.작업을 추상화한 클래스로 자체 스레드가 아닌 QThreadPool을 사용해 스레드들이 관리된다.
스레드 풀을 추상화한 클래스로, 스레드 풀 작업을 조정 및 관리한다.
재생 버튼을 누르면 애니메이션 파일(APNG)를 GUI 화면에 1회 재생하는 예제.
import time
import imageio.v3 as iio
from PySide6.QtCore import QRunnable
from PySide6.QtGui import QImage, QPixmap
class AnimationThread(QRunnable):
def __init__(self, path, recognized_signal):
super().__init__()
self.path = path
self.recognized_signal = recognized_signal
def run(self):
frames = self.load_frames(self.path)
for frame in frames:
h, w, ch = frame.shape
bytes_per_line = ch * w
qimg = QImage(frame.data, w, h, bytes_per_line, QImage.Format.Format_RGBA8888)
pixmap = QPixmap.fromImage(qimg)
self.recognized_signal.emit(pixmap)
time.sleep(0.1) # FPS
def load_frames(self, path):
# APNG 파일 로드
try:
frames = iio.imread(path, plugin="pillow", index=None) # pillow 투명도 유지
if frames is None:
raise ValueError("Unable to invoke APNG frame.")
except Exception as e:
print(f"{path} loading error: {e}")
frames = []
return frames
import sys
import os
from PySide6.QtWidgets import QApplication, QMainWindow, QGraphicsScene, QGraphicsPixmapItem
from PySide6.QtCore import QThreadPool, Signal, Slot
from PySide6.QtGui import QPixmap
from mainwindow import Ui_MainWindow
from thread.anim_thread import AnimationThread
class indexWindow(QMainWindow, Ui_MainWindow):
frame_signal = Signal(QPixmap) # 애니메이션 프레임을 전달하는 신호
def __init__(self):
super(indexWindow, self).__init__()
self.setupUi(self)
self.pushButton.clicked.connect(self.start_video) # 버튼 클릭 이벤트 연결
self.scene = QGraphicsScene(self)
self.graphicsView.setScene(self.scene)
self.graphics_item = QGraphicsPixmapItem()
self.scene.addItem(self.graphics_item)
self.thread_pool = QThreadPool()
self.frame_signal.connect(self.update_frame)
def start_video(self):
animation_thread = AnimationThread(path="file_path", recognized_signal=self.frame_signal)
self.thread_pool.start(animation_thread) # animation 스레드 시작
@Slot(QPixmap) # 애니메이션 프레임 신호를 수신
def update_frame(self, pixmap):
self.graphics_item.setPixmap(pixmap)
self.scene.setSceneRect(0, 0, pixmap.width(), pixmap.height())
def closeEvent(self, event):
self.thread_pool.clear()
self.thread_pool.waitForDone()
event.accept()
if __name__ == '__main__':
app = QApplication(sys.argv)
Window = indexWindow()
Window.show()
sys.exit(app.exec())
run(): 실제 수행될 작업. QThreadPool에 의해 QRunnable이 실행될 때 호출되는 메서드.start(QRunnable, priority=0): QRunnable 객체를 스레드 풀에 추가하고 실행. priority 값으로 우선 순위를 설정할 수 있다. 현재 작업 중인 스레드의 수가 최댓값이면 QRunnable 객체는 대기 큐에 추가된다.clear(): 대기 중인 모든 작업을 취소하고 삭제한다.waitForDone(): 스레드 풀에 있는 모든 작업이 종료될 때까지 대기