Profile picture

[PySide6] QThread에 대해서 알아보자.

JaehyoJJAng2024년 08월 19일

QThread를 사용하는 이유

  • UI 블로킹 방지: 메인 스레드(주로 UI 스레드)를 차단하는 작업을 QThread로 옮겨 실행하면, UI가 멈추지 않고 계속 반응하도록 처리할 수 있다.
  • Qt의 이벤트 루프와 자연스럽게 연동: QThread를 사용하면 Qt의 이벤트 루프, 시그널/슬롯 시스템을 그대로 활용할 수 있으므로, 스레드 간 통신 구현이 용이하다.

PySide6에서 QThread를 사용하는 이유는 주로 UI(메인 스레드) 가 블로킹 되지 않도록하여 애플리케이션의 반응을 유지하고,

동시에 처리가 비교적 긴 작업(네트웍 통신, 대용량 파일 처리, 복잡한 계산, 크롤링 등)을 백그라운드에서 처리하기 위함이다.


QThread는 언제 사용함?

  • 시간이 오래 걸리는 작업이 있는데, 해당 작업을 UI 스레드에서 수행하면 UI가 멈추는 현상이 발생할 때
  • 네트워킹(HTTP 요청, 소켓 통신 등), 파일 I/O(대용량 파일 읽기/쓰기), 복잡한 연산 등을 수행해야 할 때
  • 멀티스레딩을 통해 동시에 여러 작업을 병렬로 처리하여 퍼포먼스를 높이고 싶을 때

간단한 예제로 익숙해지기!

아래 예제는 QThread를 상속받은 Worker 클래스를 사용하여,

1초 간격으로 숫자를 세는 작업을 백그라운드에서 실행하고, 그 진행 상황을 시그널을 통해 UI에 전달하는 코드이다.

import sys
import time
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QTextEdit
from PySide6.QtCore import QThread, Signal, Slot

# QThread를 상속받은 Worker 클래스
class Worker(QThread):
    # 스레드에서 발생하는 이벤트(메시지, 진행상황 등)를 전달할 시그널 정의
    progress = Signal(str)

    def run(self):
        """실제 스레드에서 수행할 작업"""
        for i in range(1, 6):
            time.sleep(1)  # 1초 대기
            # progress 시그널로 현재 카운트 상태를 보냄
            self.progress.emit(f"Count: {i}")

class MyWidget(QWidget):
    def __init__(self):
        super().__init__()

        self.text_edit = QTextEdit()
        self.button = QPushButton("Start Thread")

        layout = QVBoxLayout()
        layout.addWidget(self.text_edit)
        layout.addWidget(self.button)
        self.setLayout(layout)

        # Worker 스레드를 하나 생성
        self.thread = Worker()
        # Worker 스레드에서 전달하는 progress 시그널을 UI 업데이트 슬롯에 연결
        self.thread.progress.connect(self.handle_progress)

        # 버튼 클릭 시 스레드 시작
        self.button.clicked.connect(self.start_thread)

    @Slot(str)
    def handle_progress(self, text):
        """스레드에서 넘어온 시그널에 따라 UI를 갱신하는 슬롯"""
        self.text_edit.append(text)

    def start_thread(self):
        """스레드를 시작하는 메서드"""
        if not self.thread.isRunning():
            self.text_edit.append("Thread started!")
            self.thread.start()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MyWidget()
    window.show()
    sys.exit(app.exec())

코드 설명

Worker(QThread)

  • QThread를 상속받은 Worker 클래스를 정의하였음.
  • progress라는 Signal을 정의하여, 진행 상황(또는 메시지)을 메인 스레드(UI)로 전달할 수 있도록 하였음.
  • run() 메서드 안에 실제 백그라운드 작업을 구현함.

스레드와 UI의 통신

  • Worker 스레드에서 반복 작업을 수행하고, 반복마다 progress 시그널을 emit 하고 있다.
  • 메인 스레드(UI)는 progress 시그널을 받아 handle_progress() 슬롯을 호출하고, 그 결과를 QTextEdit에 표시하도록 하였다.

Slot(Type)

@Slot(<type>) 데코레이터에 <type>이 명시되는 이유는,

이 슬롯 메서드가 특정 타입의 인수를 받을 것임을 Qt에 알려주기 위해서이다.
(위 예제에서는 str(문자열) 타입의 인수를 받을 것임을 명시하였음.)


Qt의 시그널/슬롯 메커니즘에서는 시그널과 슬롯 간 파라미터의 타입과 개수가 일치해야 올바르게 연결될 수 있다.

예제에서 progress = Signal(str)로 선언된 시그널이 문자열을 보내기 때문에,

슬롯 역시 같은 형태(str)의 인자를 받을 수 있도록 데코레이터에 @Slot(str)를 지정해 준거다!


  • 시그널: progress = Signal(str)
    • :문자열 타입(str) 파라미터를 가진 시그널.
  • 슬롯: @Slot(str)
    • :문자열 타입(str) 파라미터를 받을 준비가 된 슬롯.

이렇게 타입을 명시하면,

Qt 내부에서 해당 메서드를 슬롯으로 인식하는 과정(시그널/슬롯 연결 및 호출 과정)이 정확해지고,

IDE 혹은 Qt 자체도 시그널/슬롯 타입 검증을 좀 더 명확히 할 수 있게 된다.

정리하자면,

슬롯에 전달될 인수의 타입을 Qt에 명시하는 것이며, 시그널에서 str 타입의 값을 보낼 때 슬롯이 정확히 그 타입을 받도록 보장하기 위한 장치라고 이해하면 된다.


Loading script...