๐ English Abstract
This article addresses the critical issue of UI freezing caused by blocking I/O operations in PyVISA during the development of PyQt6-based test and measurement automation systems. It provides a comprehensive troubleshooting guide for implementing an asynchronous command queue using PyQt6's QThread and Python's thread-safe Queue module. Readers will learn how to architecture non-blocking VISA communication, handle edge-case timeouts gracefully, and ensure complete thread safety in high-throughput hardware test environments.
๋ฐ๋์ฒด ๊ฒ์ฌ ์ฅ๋น, ์ ์ ๋ถํ ํน์ฑ ํ๊ฐ, ๋๋ ์์ฐ ๋ผ์ธ์ ์๋ํ ์ํ ์ค๋น๋ฅผ ๊ฐ๋ฐํ ๋ Python์ PyQt6์ PyVISA ์กฐํฉ์ ์ฌ์ค์์ ํ์ค(De-facto Standard)์ผ๋ก ์๋ฆฌ ์ก์์ต๋๋ค. ๊ทธ๋ฌ๋ ๊ฐ๋ฐ ์ด๊ธฐ ๋จ์ํ ์คํฌ๋ฆฝํธ ์์ฑ ๋ฐฉ์์ ๊ทธ๋๋ก GUI(Graphical User Interface) ๊ตฌ์กฐ์ ์ด์ํ๋ค ๋ณด๋ฉด, ์ฅ๋น์ ๋ช ๋ น์ด ์์ /์ก์ ๊ณผ์ ์์ **UI๊ฐ ๋ฌด์๋ต ์ํ(Not Responding)**๋ก ๋น ์ง๊ฑฐ๋ ์ฌํ๋ฉด ์ฑ์ด ๊ฐ์ ์ข ๋ฃ๋๋ ํ์์ ๋น๋ฒํ ๊ฒช๊ฒ ๋ฉ๋๋ค.
๋ณธ ํฌ์คํ ์์๋ ๊ณ์ธก ์๋ํ ์์คํ ๊ฐ๋ฐ ์ ๋ฐ์ํ๋ PyVISA์ ๋ธ๋กํน I/O(Blocking Input/Output) ์์ธ์ ๊น์ด ์๊ฒ ๋ถ์ํ๊ณ , QThread์ ๋น๋๊ธฐ ๋ช ๋ น์ด ํ(Asynchronous Command Queue)๋ฅผ ๊ฒฐํฉํ์ฌ ์๋ฒฝํ๊ฒ ๋ฐ์ํ GUI๋ฅผ ์ ์งํ๋ ์ค๋ฌด ํธ๋ฌ๋ธ์ํ ๊ฐ์ด๋๋ฅผ ์ ๋ฌํฉ๋๋ค.
1. UI ์๋ต ์ง์ฐ์ ๊ทผ๋ณธ ์์ธ: PyVISA ๋๊ธฐ ํต์ ๊ณผ ๋ฉ์ธ ์ด๋ฒคํธ ๋ฃจํ
PyQt6๋ฅผ ํฌํจํ ๋๋ถ๋ถ์ GUI ํ๋ ์์ํฌ๋ ๋จ์ผ ์ค๋ ๋ ๊ธฐ๋ฐ ์ด๋ฒคํธ ๋ฃจํ(Single-threaded Event Loop)๋ก ์๋ํฉ๋๋ค. ์ฆ, ํ๋ฉด์ ์๋ก ๊ทธ๋ฆฌ๊ณ (Redraw), ๋ง์ฐ์ค ํด๋ฆญ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๋ฉฐ, ํค๋ณด๋ ์ ๋ ฅ์ ๋ฐ๋ ๋ชจ๋ ์์ ์ด '๋ฉ์ธ ์ค๋ ๋(Main Thread / UI Thread)' ํ๋์์ ์์ฐจ์ ์ผ๋ก ์คํ๋ฉ๋๋ค.
๋ฐ๋ฉด, ์ค์ค๋ก์ค์ฝํ, ์ ์ ๊ณต๊ธ๊ธฐ(Power Supply), ๋ฉํฐ๋ฏธํฐ(DMM) ๋ฑ์ ๊ณ์ธก๊ธฐ์ ํต์ ํ๊ธฐ ์ํด ์ฌ์ฉํ๋ PyVISA ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ๊ธฐ๋ณธ ํจ์๋ค(inst.query(), inst.read(), inst.write())์ **๋๊ธฐ์ ๋ธ๋กํน(Synchronous Blocking) ๋ฐฉ์**์ผ๋ก ๋์ํฉ๋๋ค.
- I/O ๋๊ธฐ ์๊ฐ(Latency): ๊ณ์ธก๊ธฐ๊ฐ SCPI(Standard Commands for Programmable Instruments) ๋ช ๋ น์ด๋ฅผ ํด์ํ๊ณ ๋ฐ์ดํฐ๋ฅผ ๋ณํํ๋ ๋ฐ ์์ญ ๋ฐ๋ฆฌ์ด(ms)์์ ์ ์ด(s)๊ฐ ์์๋ฉ๋๋ค.
- ์ด๋ฒคํธ ๋ฃจํ ์ฐจ๋จ: ๋ฉ์ธ ์ค๋ ๋์์
query('*IDN?')๋ ๋ฐ์ดํฐ ๋ก๊น ํจ์๋ฅผ ์ง์ ํธ์ถํ๋ฉด, ๊ณ์ธก๊ธฐ๋ก๋ถํฐ ์๋ต์ด ๋์์ฌ ๋๊น์ง ๋ฉ์ธ ์ค๋ ๋๊ฐ ์์ ํ ๋ฉ์ถฅ๋๋ค(Block). - ๊ฒฐ๊ณผ: ์ฌ์ฉ์๋ ์ฐฝ ์ด๋, ๋ฒํผ ํด๋ฆญ, ๊ทธ๋ํฝ ์ฐจํธ ์ ๋ฐ์ดํธ ๋ฑ ๋ชจ๋ GUI ๋ฐ์์ด ์ผ์ ์ ์ง๋๋ ์ด๋ฅธ๋ฐ 'UI ๋ฉ์ถค ํ์'์ ๊ฒฝํํ๊ฒ ๋ฉ๋๋ค.
๋จ์ํ Python์ ๊ธฐ๋ณธ threading ๋ชจ๋์ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ, PyQt6์ GUI ์์๋ฅผ ์๋ธ ์ค๋ ๋์์ ์ง์ ์์ ํ๋ ค๋ค **์ค๋ ๋ ์ธ์ดํํฐ(Thread Safety)** ์๋ฐ์ผ๋ก ์ธํด ์ ํ๋ฆฌ์ผ์ด์
์ด ๋ฉ๋ชจ๋ฆฌ ํฌ๋์(Segmentation Fault)๋ฅผ ์ผ์ผํค๋ 2์ฐจ ๋ฌธ์ ๋ก ์ด์ด์ง๋๋ค.
2. ์ํคํ ์ฒ ์ค๊ณ: ์์ฐ์-์๋น์ ํจํด ๊ธฐ๋ฐ์ ๋น๋๊ธฐ Queue ์์คํ
์ด ๋ฌธ์ ๋ฅผ ๊ทผ๋ณธ์ ์ผ๋ก ํด๊ฒฐํ๋ ค๋ฉด ๊ณ์ธก๊ธฐ ํต์ ์ ์ฉ ๋น๋๊ธฐ Worker ์ค๋ ๋๋ฅผ ๋ถ๋ฆฌํ๊ณ , ๋ฉ์ธ ์ค๋ ๋์ ํต์ ์ค๋ ๋ ์ฌ์ด์ **์์ฐ์-์๋น์ ํจํด(Producer-Consumer Pattern)**์ ์ ์ฉํ ์์ ํ ๋ฉ์์ง ์ ๋ฌ ํ์ดํ๋ผ์ธ์ ๊ตฌ์ถํด์ผ ํฉ๋๋ค.
์ ์ฒด ์์คํ ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ์ด ์ค๊ณ๋ฉ๋๋ค.
- UI Thread (Producer): ์ฌ์ฉ์์ ๋ฒํผ ํด๋ฆญ์ด๋ ํ์ด๋จธ ๊ธฐ๋ฐ ๋ก๊น
์์ฒญ์ **๋ช
๋ น์ด ๊ฐ์ฒด(Command Data Class)**๋ก ์์ฑํ์ฌ
queue.Queue์ ์ฝ์ (Put)ํฉ๋๋ค. - Thread-Safe Queue: ๋ช ๋ น์ด์ ํ๋ผ๋ฏธํฐ, ์ฝ๋ฐฑ ์ ๋ณด๋ฅผ ๋ด๋ FIFO(First-In-First-Out) ๋๊ธฐ์ด์ ๋๋ค. ์ค๋ ๋ ๊ฐ ๋๊ธฐํ๋ฅผ ์์ ํ๊ฒ ์ฒ๋ฆฌํฉ๋๋ค.
- Worker QThread (Consumer): ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋ฌดํ ๋ฃจํ๋ฅผ ๋๋ฉฐ ํ์ ๋ช ๋ น์ด๊ฐ ๋ค์ด์ค๋์ง ๊ฐ์ํ๋ค๊ฐ, ๋ช ๋ น์ด๊ฐ ๋์ฐฉํ๋ฉด PyVISA ๋๊ธฐ ํต์ ์ ์คํํฉ๋๋ค.
- PyQt Signal/Slot: ํต์ ๊ฒฐ๊ณผ(์ธก์ ๊ฐ, ์ํ ๋ฉ์์ง, ์๋ฌ ๋ด์ญ)๋ฅผ PyQt6์ ์๊ทธ๋์ ํตํด ์์ ํ๊ฒ UI ์ค๋ ๋๋ก ์ ๋ฌ(Emit)ํ์ฌ ํ๋ฉด์ ์ ๋ฐ์ดํธํฉ๋๋ค.
3. ์ค์ ๊ตฌํ: QThread ๋ฐ PyVISA ๋น๋๊ธฐ ๋ช ๋ น์ด ํ ์ฝ๋
์๋ ์ฝ๋๋ ๋ฐ๋ก ํ์ ํ๋ก์ ํธ์ ์ ์ฉํ ์ ์๋๋ก ์ค๊ณ๋ ํด๋์ค ๊ตฌ์กฐ์ ๋๋ค. PyVISA ์ฒ๋ฆฌ์ฉ Worker ์ค๋ ๋์ ์ปค์คํ ์๊ทธ๋, ๊ทธ๋ฆฌ๊ณ ํ ์์คํ ์ ํฌํจํ๊ณ ์์ต๋๋ค.
import sys
import time
import queue
from dataclasses import dataclass
from typing import Any, Optional
import pyvisa
from PyQt6.QtCore import QThread, pyqtSignal, QObject
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QTextEdit, QVBoxLayout, QWidget
# 1. ๋ช
๋ น์ด ๊ตฌ์กฐ์ฒด ์ ์
@dataclass
class VisaCommand:
cmd_type: str # 'WRITE', 'QUERY', 'READ'
command: str
kwargs: dict = None
# 2. PyVISA ์ ์ฉ Worker Thread ๊ตฌํ
class InstrumentWorker(QThread):
# GUI๋ก ๊ฒฐ๊ณผ๋ฅผ ์ ๋ฌํ๊ธฐ ์ํ PyQt ์๊ทธ๋
data_received = pyqtSignal(str, str) # (command, response)
error_occurred = pyqtSignal(str, str) # (command, error_message)
status_changed = pyqtSignal(str) # status updates
def __init__(self, resource_name: str, parent: Optional[QObject] = None):
super().__init__(parent)
self.resource_name = resource_name
self.cmd_queue = queue.Queue()
self._is_running = True
self.rm = None
self.inst = None
def run(self):
"""๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋ ๋ฉ์ธ ๋ฃจํ"""
try:
self.rm = pyvisa.ResourceManager()
self.inst = self.rm.open_resource(self.resource_name)
self.inst.timeout = 3000 # 3์ด ํ์์์ ์ค์
self.status_changed.emit(f"์ฐ๊ฒฐ ์ฑ๊ณต: {self.resource_name}")
except Exception as e:
self.error_occurred.emit("CONNECT", f"์ฅ๋น ์ฐ๊ฒฐ ์คํจ: {str(e)}")
return
while self._is_running:
try:
# Queue์์ ๋ช
๋ น์ด๋ฅผ ๊บผ๋ (0.1์ด ํ์์์์ผ๋ก ๋ฃจํ ํ์ถ ๊ฐ๋ฅํ๊ฒ ํจ)
cmd_obj: VisaCommand = self.cmd_queue.get(timeout=0.1)
except queue.Empty:
continue
# ๋ช
๋ น์ด ์ฒ๋ฆฌ (PyVISA ๋๊ธฐ ํธ์ถ ๋ธ๋กํน ๊ตฌ๊ฐ)
try:
if cmd_obj.cmd_type == 'QUERY':
res = self.inst.query(cmd_obj.command)
self.data_received.emit(cmd_obj.command, res.strip())
elif cmd_obj.cmd_type == 'WRITE':
self.inst.write(cmd_obj.command)
self.data_received.emit(cmd_obj.command, "OK")
except pyvisa.VisaIOError as e:
self.error_occurred.emit(cmd_obj.command, f"VISA I/O ์๋ฌ: {e.description}")
except Exception as e:
self.error_occurred.emit(cmd_obj.command, f"์ผ๋ฐ ์์ธ: {str(e)}")
finally:
self.cmd_queue.task_done()
# ์์ ํด์
if self.inst:
self.inst.close()
if self.rm:
self.rm.close()
self.status_changed.emit("ํต์ ์ค๋ ๋ ์ข
๋ฃ ์๋ฃ")
def send_command(self, cmd_type: str, command: str):
"""UI ์ค๋ ๋์์ ํธ์ถํ๋ ๋ช
๋ น์ด ํฌ์
๋ฉ์๋"""
self.cmd_queue.put(VisaCommand(cmd_type, command))
def stop(self):
"""์ค๋ ๋ ์์ ์ข
๋ฃ"""
self._is_running = False
self.wait()
๋ฉ์ธ UI ์ธํฐํ์ด์ค ๊ฒฐํฉ
์์ฑํ InstrumentWorker๋ฅผ PyQt6 ๋ฉ์ธ ์๋์ฐ์ ์ฐ๊ฒฐํ๋ ์ฝ๋์
๋๋ค. UI ์ด๋ฒคํธ๊ฐ ์ค๋ ๋๋ฅผ ๋ฐฉํดํ์ง ์๊ณ ํ์ ์ฐจ๊ณก์ฐจ๊ณก ์์ด๋ ๊ตฌ์กฐ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("PyQt6 + PyVISA ๋น๋๊ธฐ ๊ณ์ธก ์ ์ด๊ธฐ")
self.resize(600, 400)
# UI ๋ ์ด์์ ๊ตฌ์ฑ
layout = QVBoxLayout()
self.log_widget = QTextEdit()
self.log_widget.setReadOnly(True)
self.btn_idn = QPushButton("*IDN? ์๋ณ ์ฝ๋ ์กฐํ")
self.btn_measure = QPushButton("VOLT? ์ ์ ์ธก์ (์ฐ์ ์์ฒญ)")
layout.addWidget(self.log_widget)
layout.addWidget(self.btn_idn)
layout.addWidget(self.btn_measure)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
# Worker ์ค๋ ๋ ์์ฑ ๋ฐ ์๊ทธ๋ ์ฐ๊ฒฐ
# ์ค์ ํ๊ฒฝ์ ๋ง๋ VISA Resource Name ์
๋ ฅ (์: 'GPIB0::14::INSTR' ๋๋ 'TCPIP::192.168.1.100::INSTR')
self.worker = InstrumentWorker('ASRL1::INSTR')
self.worker.data_received.connect(self.handle_data)
self.worker.error_occurred.connect(self.handle_error)
self.worker.status_changed.connect(self.handle_status)
# ์ด๋ฒคํธ ๋ฒํผ ์ฐ๊ฒฐ
self.btn_idn.clicked.connect(lambda: self.worker.send_command('QUERY', '*IDN?'))
self.btn_measure.clicked.connect(self.trigger_multiple_queries)
# ์ค๋ ๋ ์์
self.worker.start()
def trigger_multiple_queries(self):
# ์ฐ์๋ ๋ช
๋ น์ด๋ฅผ ํ์ ๋ฃ์ด๋ UI๊ฐ ์ ํ ๋ฉ์ถ์ง ์์
for _ in range(5):
self.worker.send_command('QUERY', 'MEAS:VOLT:DC?')
def handle_data(self, cmd, response):
self.log_widget.append(f"[์์ ] {cmd} -> {response}")
def handle_error(self, cmd, error):
self.log_widget.append(f"[์๋ฌ] {cmd}: {error}")
def handle_status(self, status):
self.log_widget.append(f"[์ํ] {status}")
def closeEvent(self, event):
# ์ฑ ์ข
๋ฃ ์ Worker ์ค๋ ๋๋ฅผ ์์ ํ๊ฒ cleanup
self.worker.stop()
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
4. ํ์ ํธ๋ฌ๋ธ์ํ ๋ฐ ์ฃ์ง ์ผ์ด์ค ์ฒ๋ฆฌ
์์ ๊ธฐ๋ณธ์ ์ธ ๋น๋๊ธฐ ํ ๊ตฌ์กฐ๋ฅผ ๊ตฌ์ถํ๋๋ผ๋ ์ค์ ํ๋์จ์ด ๊ฒ์ฌ ํ์ฅ์์๋ ๋ค์ํ ์์ธ๊ฐ ๋ฐ์ํฉ๋๋ค. ์์ ์ฑ์ ๋ํญ ํฅ์์ํค๊ธฐ ์ํ ํ์ ์ฃ์ง ์ผ์ด์ค(Edge Cases) ํด๊ฒฐ ๋ฐฉ์์ ๋๋ค.
๊ฐ. VISA Timeout ๋ฐ ๋ฒ์ค ๋ก์ (Lockup) ๋์
๊ณ์ธก๊ธฐ ์ ์์ด ๊บผ์ง๊ฑฐ๋ ์ผ์ด๋ธ์ด ์ดํ๋ ๊ฒฝ์ฐ inst.query()๋ ๋ฌดํ์ ๋๊ธฐํ๊ฑฐ๋ ํ์์์ ์์ธ(VI_ERROR_TMO)๋ฅผ ๋ฐ์์ํต๋๋ค. ํ์์์์ด ๋ฐ์ํ๋ฉด VISA ๋ฆฌ์์ค ๋ฒ์ค์ ์์ฌ ๋ฐ์ดํฐ(Stale Data)๊ฐ ๋จ์ ๋ค์ ๋ช
๋ น์ด๊ฐ ์ํค๊ฒ ๋ฉ๋๋ค.
- ํด๊ฒฐ์ฑ
: ์์ธ ์ฒ๋ฆฌ(
except pyvisa.VisaIOError) ๋ธ๋ก ๋ด๋ถ์์inst.clear()๋ฅผ ํธ์ถํ์ฌ ํ๋์จ์ด ๋ฒ์ค ๋ฒํผ๋ฅผ ์ฆ์ ๋น์์ค์ผ ํฉ๋๋ค. - ๋ช
๋ น์ด ์ํ ์ ํ์ ํ์์์ ์์น๋ฅผ ์ ๋์ ์ผ๋ก ์กฐ์ (์: ์ผ๋ฐ ์กฐํ์ ๊ฒฝ์ฐ 1์ด, Sweep ์ธก์ ์ ๊ฒฝ์ฐ 10์ด)ํ๋ ๊ฐ๋ณ ํ์์์(Dynamic Timeout)์
VisaCommand๋ฐ์ดํฐ ํด๋์ค์ ํฌํจ์ํค๋ ๊ฒ์ด ์ข์ต๋๋ค.
๋. ํ ์ค๋ฒํ๋ก์ฐ(Queue Overflow) ๋ฐ Backpressure ์ ์ด
GUI ํ์ด๋จธ๊ฐ 10ms ๋จ์๋ก ๋น ๋ฅด๊ฒ ์ธก์ ๋ช ๋ น์ ํ์ ์ฝ์ ํ๋๋ฐ, ๊ณ์ธก๊ธฐ์ ์๋ต ์๋๊ฐ 50ms๋ผ๋ฉด ํ์ ๋ฐ์ดํฐ๊ฐ ๋ฌดํํ ์์ฌ **๋ฉ๋ชจ๋ฆฌ ๋์(Memory Leak)**์ **๋ฐ์ดํฐ ์์ฐจ ์ง์ฐ(Latency Shift)**์ด ๋ฐ์ํฉ๋๋ค.
- ํด๊ฒฐ์ฑ
:
queue.Queue(maxsize=100)๊ณผ ๊ฐ์ด ์ต๋ ํ ํฌ๊ธฐ๋ฅผ ์ ํํ๊ฑฐ๋, ํ์ ๋์ผํ ์ ํ์ ์กฐํ ๋ช ๋ น์ด ์ด๋ฏธ ๋จ์ ์๋ค๋ฉด ๊ธฐ์กด ๋ช ๋ น์ ๋ฎ์ด์ฐ๊ฑฐ๋ ๋ฌด์ํ๋ **์ต์ ๋ฐ์ดํฐ ์ฐ์ ์์ ์ ์ฑ (Latest-Only Policy)**์ ์ ์ฉํด์ผ ํฉ๋๋ค.
๋ค. ์์ ํ ์ค๋ ๋ ์ข ๋ฃ (Graceful Shutdown)
์ฌ์ฉ์๊ฐ ํ๋ก๊ทธ๋จ X ๋ฒํผ์ ๋๋ฌ ์ข ๋ฃํ ๋ ๊ณ์ธก๊ธฐ์ ํต์ ์ค์ธ ๊ฒฝ์ฐ ํ๋ก์ธ์ค๊ฐ ๊ณ ์คํธ ์ค๋ ๋๋ก ๋จ์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ฃฝ์ง ์๋ ํ์์ด ์์ฃผ ์ผ์ด๋ฉ๋๋ค.
- ํด๊ฒฐ์ฑ
: ๋ฉ์ธ ์๋์ฐ์
closeEvent์์ ๋จ์terminate()๋ฅผ ํธ์ถํ๋ฉด ๊ฐ์ ์ข ๋ฃ๋ก ์ธํด VISA ๋๋ผ์ด๋ฒ ์ธ์ ์ด ์ด๋ฆฐ ์ฑ ์ ๊ฒจ๋ฒ๋ฆฝ๋๋ค. ๋ฐ๋์_is_runningํ๋๊ทธ๋ฅผ ๋ณ๊ฒฝํ๊ณwait(msecs=2000)๋ฐฉ์์ผ๋ก ์ค๋ ๋๊ฐ ์ ์ ์ ๋ฆฌ๋ ๋๊น์ง ๋๊ธฐ ํ ์ข ๋ฃํด์ผ ํฉ๋๋ค.
5. ๊ฒฐ๋ก ๋ฐ ์ฑ๋ฅ ์ต์ ํ ์์ฝ
PyQt6 ๊ธฐ๋ฐ์ ๊ณ์ธก ์๋ํ ํ๋ก๊ทธ๋จ ๊ฐ๋ฐ ์ PyVISA์ ๋๊ธฐ์ ํต์ ํน์ฑ์ ์ดํดํ๊ณ ์ด๋ฅผ ๋ฉ์ธ UI ์ค๋ ๋๋ก๋ถํฐ ๋ถ๋ฆฌํ๋ ๊ฒ์ ์์ ์ ์ธ ์ค๋น ์ํํธ์จ์ด ๊ตฌ์ถ์ ํต์ฌ์ ๋๋ค.
| ๊ตฌ๋ถ | ๊ธฐ์กด ๋๊ธฐ ํต์ ๋ฐฉ์ | QThread + Queue ๋น๋๊ธฐ ๋ฐฉ์ |
|---|---|---|
| UI ๋ฐ์์ฑ | ๋ช ๋ น์ด ์ ์ก ์ค ์ ์ฒด UI ๋ฉ์ถค (๋ฌด์๋ต) | ํญ์ 60FPS ์์ค์ ๋งค๋๋ฌ์ด ๋ฐ์์ฑ ์ ์ง |
| ์์ธ ์ฒ๋ฆฌ | ํต์ ์ค๋ฅ ์ GUI ์ ์ฒด ํฌ๋์ ๋ฐ์ ๊ฐ๋ฅ | ์ค๋ ๋ ๋ด๋ถ ์บ์น ํ UI ์๊ทธ๋๋ก ์์ ํ๊ฒ ์๋ฌ ๋ฆฌํฌํ |
| ํ์ฅ์ฑ | ๋ค์ค ์ฅ๋น ์ ์ด ์ ๊ทน์ฌํ ๋ณ๋ชฉ ํ์ | ์ฅ๋น๋ณ ๋ ๋ฆฝ Worker Thread ํ์ฅ ์ฉ์ด |
๋ณธ ๊ฐ์ด๋์์ ๋ค๋ฃฌ QThread ๋น๋๊ธฐ ๋ช ๋ น์ด ํ ํจ๋ฌ๋ค์์ ๋์ ํ๋ฉด, ์์ญ ๋์ ๊ณ์ธก๊ธฐ๋ฅผ ๋์์ ์ ์ดํ๊ฑฐ๋ ๊ณ ์ ๋ฐ์ดํฐ ๋ก๊น ์ ์ํํ๋ ํ๊ฒฝ์์๋ ์ฌ์ฉ์๋ ์พ์ ํ๊ณ ์์ ์ ์ผ๋ก ๋ชจ๋ํฐ๋ง ์์คํ ์ ์ฌ์ฉํ ์ ์์ต๋๋ค. ๊ตฌํ ๊ณผ์ ์์ ๋ฐ์ํ ์ ์๋ ํ์์์ ์ธ์ ํด๋ฆฌ์ด ์กฐ์น์ ์ค๋ ๋ ์๋ช ์ฃผ๊ธฐ ๊ด๋ฆฌ๊น์ง ๊ผผ๊ผผํ ์ ์ฉํด ๋ณด์๊ธฐ ๋ฐ๋๋๋ค.