Profile picture

[PySide6] 스레드 일시 중지, 재개, 정지 기능 구현해보기

JaehyoJJAng2024년 10월 01일

개요

스레드에서 반복 작업을 수행할 때, 사용자가 "일시 중지" 버튼을 누르면 현재 진행 중인 작업을 멈추고,

"재개" 버튼을 누르면 중단한 작업을 계속 진행할 수 있어야 합니다.

또한 "정지" 버튼을 누르면 스레드를 완전히 종료할 수 있도록 구현해야하죠.


이를 구현하기 위해 파이썬의 threading.Event를 사용하려고 합니다.

Event 객체는 내부 플래그의 설정 여부를 통해 스레드의 진행을 제어할 수 있으며, 다음과 같이 사용합니다.

  • 일시 중지: pause_event.clear()를 호출하여 플래그를 초기화하면, 스레드는 pause_event.wait()에서 블록 상태에 들어갑니다.
  • 재개: pause_event.set()을 호출하면, 대기 상태에 있던 스레드가 다시 실행돼요.
  • 정지: 별도의 stop_event를 사용하여 스레드 루프 내에서 종료 조건을 확인합니다.

예제 코드

1. for문 로직

import sys
import time
from threading import Event
from PySide6.QtCore import QThread, Signal
from PySide6.QtWidgets import (
    QApplication, QWidget, QPushButton, QVBoxLayout, QTextEdit
)

class WorkerThread(QThread):
    # 작업 진행 상황을 UI에 전달하기 위한 시그널
    log = Signal(str)
    finished = Signal()

    def __init__(self):
        super().__init__()
        # 작업 중 일시 중지와 정지 제어를 위한 Event 객체
        self._pause_event = Event()
        self._stop_event = Event()
        self._pause_event.set()  # 시작 시에는 실행 상태로 설정

    def run(self):
        for i in range(100):
            # 정지 요청이 들어오면 바로 종료
            if self._stop_event.is_set():
                self.log.emit("스레드 정지 요청이 들어와 종료합니다.")
                return

            # 일시 중지 상태이면 _pause_event가 set 될 때까지 대기
            self._pause_event.wait()

            # 실제 작업 (예: 데이터 처리, 네트워크 요청 등)
            self.log.emit(f"항목 {i} 처리 중...")
            time.sleep(0.5)  # 작업의 진행 상황을 확인하기 위한 딜레이

        self.log.emit("작업이 완료되었습니다.")
        self.finished.emit()

    def pause(self):
        """스레드를 일시 중지합니다."""
        self._pause_event.clear()
        self.log.emit("스레드가 일시 중지되었습니다.")

    def resume(self):
        """일시 중지된 스레드를 재개합니다."""
        self._pause_event.set()
        self.log.emit("스레드가 재개되었습니다.")

    def stop(self):
        """스레드 작업을 중지합니다."""
        self._stop_event.set()
        # 일시 중지 상태에서 정지할 경우 wait() 해제
        self._pause_event.set()
        self.log.emit("스레드 정지를 요청했습니다.")

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("스레드 일시중지/재개/정지 예제")

        # UI 구성요소 생성
        self.start_btn = QPushButton("시작")
        self.pause_btn = QPushButton("일시 중지")
        self.resume_btn = QPushButton("재개")
        self.stop_btn = QPushButton("정지")
        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)

        # 레이아웃 구성
        layout = QVBoxLayout()
        layout.addWidget(self.start_btn)
        layout.addWidget(self.pause_btn)
        layout.addWidget(self.resume_btn)
        layout.addWidget(self.stop_btn)
        layout.addWidget(self.log_text)
        self.setLayout(layout)

        # WorkerThread 초기화
        self.worker = WorkerThread()
        self.worker.log.connect(self.update_log)
        self.worker.finished.connect(self.on_finished)

        # 버튼 시그널 연결
        self.start_btn.clicked.connect(self.start_worker)
        self.pause_btn.clicked.connect(self.pause_worker)
        self.resume_btn.clicked.connect(self.resume_worker)
        self.stop_btn.clicked.connect(self.stop_worker)

    def start_worker(self):
        if not self.worker.isRunning():
            self.log_text.append("스레드 시작!")
            # 스레드 시작 전, 정지 이벤트 초기화 (이미 정지된 경우 대비)
            self.worker._stop_event.clear()
            self.worker._pause_event.set()
            self.worker.start()
        else:
            self.log_text.append("스레드가 이미 실행 중입니다.")

    def pause_worker(self):
        if self.worker.isRunning():
            self.worker.pause()
        else:
            self.log_text.append("실행 중인 스레드가 없습니다.")

    def resume_worker(self):
        if self.worker.isRunning():
            self.worker.resume()
        else:
            self.log_text.append("실행 중인 스레드가 없습니다.")

    def stop_worker(self):
        if self.worker.isRunning():
            self.worker.stop()
        else:
            self.log_text.append("실행 중인 스레드가 없습니다.")

    def update_log(self, message):
        self.log_text.append(message)

    def on_finished(self):
        self.log_text.append("스레드 작업이 모두 완료되었습니다.")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.resize(400, 300)
    window.show()
    sys.exit(app.exec())

코드 설명

WorkerThread 클래스

  • 1. Event 객체 생성
    • self._pause_event: 스레드 일시 중지와 재개를 제어합니다. 초기에는 set() 되어 있어 작업이 바로 진행됩니다.
    • self._stop_event: 스레드 종료를 제어합니다.

  • 2. run() 메서드
    • 반복문 내에서 매 반복마다 _stop_event의 상태를 확인해 정지 요청이 들어오면 바로 종료합니다.
    • _pause_event.wait()를 호출하여, 만약 일시 중지 상태라면 _pause_event.set()이 호출될 때까지 대기합니다.
    • 실제 작업을 수행하고 진행 상황을 log 시그널을 통해 UI에 전달합니다.

  • 3. pause(), resume(), stop() 메서드
    • pause(): _pause_event.clear()를 호출해 스레드를 일시 중지하고, UI에 상태 메시지를 전송합니다.
    • resume(): _pause_event.set()을 호출해 대기 상태에 있는 스레드를 재개합니다.
    • stop(): _stop_event.set()을 호출해 스레드 종료를 요청하고, 혹시 일시 중지 상태라면 _pause_event.set()으로 대기 해제합니다.

MainWindow 클래스

  • 1. UI 구성 및 버튼 연결
    • 네 개의 버튼(시작, 일시 중지, 재개, 정지)과 로그를 출력할 QTextEdit를 배치합니다.
    • 각 버튼 클릭 시, WorkerThread의 해당 메서드(pause(), resume(), stop())가 호출되도록 연결합니다.

2. 스레드 시작 및 상태 업데이트

  • 스레드가 실행 중인지 확인한 후 시작하며, 작업 진행 상황은 WorkerThread의 log 시그널을 통해 QTextEdit에 출력됩니다.

2. While문 로직 (무한 반복 로직일 때)

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

class InfiniteWorker(QThread):
    # UI로 로그를 보내기 위한 시그널 정의
    log = Signal(str)
    finished = Signal()

    def __init__(self):
        super().__init__()
        self._pause_event = Event()
        self._stop_event = Event()
        self._pause_event.set()  # 시작 시에는 실행 상태로 설정

    def run(self):
        counter = 0
        while True:
            # 정지 요청이 들어오면 루프 종료
            if self._stop_event.is_set():
                self.log.emit("스레드 정지 요청됨. 작업 종료.")
                break

            # 일시 중지 요청이 들어왔으면 대기 상태
            self._pause_event.wait()

            # 실제 작업 수행 (여기서는 간단하게 카운트 증가를 예로 들음)
            self.log.emit(f"작업 진행 중... 카운터: {counter}")
            counter += 1
            time.sleep(1)  # 작업 진행을 느리게 보이기 위한 딜레이

        self.finished.emit()

    def pause(self):
        """스레드를 일시 중지합니다."""
        self._pause_event.clear()
        self.log.emit("스레드 일시 중지됨.")

    def resume(self):
        """일시 중지된 스레드를 재개합니다."""
        self._pause_event.set()
        self.log.emit("스레드 재개됨.")

    def stop(self):
        """스레드 작업을 중지합니다."""
        self._stop_event.set()
        # 만약 일시 중지 상태라면 wait() 해제
        self._pause_event.set()
        self.log.emit("스레드 정지 요청됨.")

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("무한 루프 스레드 제어 예제")

        self.start_btn = QPushButton("시작")
        self.pause_btn = QPushButton("일시 중지")
        self.resume_btn = QPushButton("재개")
        self.stop_btn = QPushButton("정지")
        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)

        layout = QVBoxLayout()
        layout.addWidget(self.start_btn)
        layout.addWidget(self.pause_btn)
        layout.addWidget(self.resume_btn)
        layout.addWidget(self.stop_btn)
        layout.addWidget(self.log_text)
        self.setLayout(layout)

        self.worker = InfiniteWorker()
        self.worker.log.connect(self.update_log)
        self.worker.finished.connect(self.on_finished)

        self.start_btn.clicked.connect(self.start_worker)
        self.pause_btn.clicked.connect(self.pause_worker)
        self.resume_btn.clicked.connect(self.resume_worker)
        self.stop_btn.clicked.connect(self.stop_worker)

    def start_worker(self):
        if not self.worker.isRunning():
            self.log_text.append("스레드 시작!")
            # 스레드 시작 전에 이벤트 상태를 초기화
            self.worker._stop_event.clear()
            self.worker._pause_event.set()
            self.worker.start()
        else:
            self.log_text.append("스레드가 이미 실행 중입니다.")

    def pause_worker(self):
        if self.worker.isRunning():
            self.worker.pause()
        else:
            self.log_text.append("실행 중인 스레드가 없습니다.")

    def resume_worker(self):
        if self.worker.isRunning():
            self.worker.resume()
        else:
            self.log_text.append("실행 중인 스레드가 없습니다.")

    def stop_worker(self):
        if self.worker.isRunning():
            self.worker.stop()
        else:
            self.log_text.append("실행 중인 스레드가 없습니다.")

    def update_log(self, message):
        self.log_text.append(message)

    def on_finished(self):
        self.log_text.append("스레드가 종료되었습니다.")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.resize(400, 300)
    window.show()
    sys.exit(app.exec())

코드 설명

InfiniteWorker 클래스

  • 1. 플래그 초기화
    • self._pause_event는 스레드가 실행 중인지(대기 상태가 아닌지) 확인하는 역할을 하며, 처음에는 set()되어 있어 바로 실행됩니다.
    • self._stop_event는 스레드 종료 요청 여부를 확인하는데 사용됩니다.

  • 2. run() 메서드
    • while True 무한 루프 내에서 매 반복마다 먼저 _stop_event를 확인해 종료 여부를 판단합니다.
    • self._pause_event.wait()clear()가 호출되면 대기 상태에 들어가고, set()이 호출되면 대기에서 벗어나 작업을 계속하도록 합니다.
  • 실제 작업은 간단한 카운터 증가와 로그 출력으로 예시하였으며, time.sleep(1)을 통해 반복 간의 간격을 주어 로그가 출력되는 것을 확인할 수 있습니다.

  • 3. 제어 메서드 (pause, resume, stop)
    • pause(): _pause_event.clear()를 호출하여 스레드를 일시 중지시키고, UI에 상태 메시지를 전달합니다.
    • resume(): _pause_event.set()을 호출하여 일시 중지 상태의 스레드를 재개합니다.
    • stop(): _stop_event.set()으로 종료 요청을 보내고, 만약 스레드가 일시 중지 상태라면 _pause_event.set()을 호출해 대기를 해제하여 루프를 종료하도록 합니다.

MainWindow 클래스

  • 1. UI 구성 및 연결
    • "시작", "일시 중지", "재개", "정지" 버튼과 로그를 출력할 QTextEdit를 사용해 간단한 UI를 구성했습니다.
    • 각 버튼은 InfiniteWorker의 해당 제어 메서드를 호출하도록 연결되어 있습니다.

  • 2. 스레드 상태 관리
    • 스레드가 실행 중인지 확인한 후 시작, 일시 중지, 재개, 정지 등의 작업을 수행합니다.
    • 작업 진행 상황은 log 시그널을 통해 QTextEdit에 출력됩니다.

Loading script...