185 lines
7.8 KiB
Python
185 lines
7.8 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
异步分发器
|
||
─────────────────────────────────────────────────────────────────
|
||
基于 QThreadPool + QRunnable 实现:
|
||
1. 接收 input_received 信号
|
||
2. 在工作线程中执行处理器链(Pipeline)
|
||
3. 处理完成后 emit output_ready 信号
|
||
线程间通信完全由 Qt 信号槽队列保证安全
|
||
─────────────────────────────────────────────────────────────────
|
||
"""
|
||
|
||
import logging
|
||
import traceback
|
||
from typing import List, Dict, Callable
|
||
|
||
from PyQt5.QtCore import QObject, QRunnable, QThreadPool, pyqtSlot, pyqtSignal
|
||
from uas.io.base_io import IOData, IOType
|
||
from uas.io.signals import io_signals
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────
|
||
# Worker:在线程池中执行处理器链
|
||
# ─────────────────────────────────────────────────────────────────
|
||
|
||
class _WorkerSignals(QObject):
|
||
"""Worker 内部信号(QRunnable 不能直接定义 pyqtSignal)"""
|
||
finished = pyqtSignal(object) # IOData
|
||
error = pyqtSignal(str, str) # data_id, error_msg
|
||
|
||
|
||
class _PipelineWorker(QRunnable):
|
||
"""
|
||
在 QThreadPool 工作线程中执行处理器链
|
||
每个 IOData 对应一个独立 Worker 实例
|
||
"""
|
||
|
||
def __init__(self, data: IOData, processors: List[Callable]):
|
||
super().__init__()
|
||
self.data = data
|
||
self.processors = processors
|
||
self.signals = _WorkerSignals()
|
||
self.setAutoDelete(True)
|
||
|
||
@pyqtSlot()
|
||
def run(self):
|
||
data = self.data
|
||
try:
|
||
data.mark_processing()
|
||
logger.debug(f"Worker 开始处理: {data}")
|
||
|
||
# 顺序执行处理器链
|
||
result = data.raw
|
||
for idx, processor in enumerate(self.processors):
|
||
io_signals.progress_updated.emit(
|
||
data.data_id,
|
||
int((idx / len(self.processors)) * 100)
|
||
)
|
||
result = processor(result, data)
|
||
|
||
data.mark_success(result)
|
||
io_signals.progress_updated.emit(data.data_id, 100)
|
||
logger.debug(f"Worker 处理完成: {data}")
|
||
self.signals.finished.emit(data)
|
||
|
||
except Exception as e:
|
||
msg = f"{type(e).__name__}: {e}\n{traceback.format_exc()}"
|
||
data.mark_error(msg)
|
||
logger.error(f"Worker 处理异常: {msg}")
|
||
self.signals.error.emit(data.data_id, msg)
|
||
|
||
|
||
# ─────────────────────────────────────────────────────────────────
|
||
# 分发器主类
|
||
# ─────────────────────────────────────────────────────────────────
|
||
|
||
class IODispatcher(QObject):
|
||
"""
|
||
IO 异步分发器
|
||
─────────────────────────────────────────────────────────────
|
||
使用方式:
|
||
dispatcher = IODispatcher()
|
||
dispatcher.register_processor(IOType.TEXT, clean_text)
|
||
dispatcher.register_processor(IOType.TEXT, translate_text)
|
||
dispatcher.start()
|
||
─────────────────────────────────────────────────────────────
|
||
"""
|
||
|
||
def __init__(self, max_threads: int = 8, parent=None):
|
||
super().__init__(parent)
|
||
self._pool: QThreadPool = QThreadPool.globalInstance()
|
||
self._pool.setMaxThreadCount(max_threads)
|
||
# { IOType: [processor_fn, ...] }
|
||
self._processors: Dict[str, List[Callable]] = {}
|
||
self._active = False
|
||
logger.info(f"IODispatcher 初始化,最大线程数={max_threads}")
|
||
|
||
# ── 处理器注册 ────────────────────────────────────────────────
|
||
|
||
def register_processor(
|
||
self,
|
||
io_type: IOType,
|
||
processor: Callable,
|
||
index: int = -1,
|
||
) -> None:
|
||
"""
|
||
注册处理器到指定 IO 类型的处理链
|
||
:param io_type: 目标类型(IOType.TEXT 等)
|
||
:param processor: fn(raw, IOData) -> processed_result
|
||
:param index: 插入位置,-1 表示追加到末尾
|
||
"""
|
||
key = io_type.value
|
||
if key not in self._processors:
|
||
self._processors[key] = []
|
||
if index == -1:
|
||
self._processors[key].append(processor)
|
||
else:
|
||
self._processors[key].insert(index, processor)
|
||
logger.info(f"注册处理器: {processor.__name__} -> {key}")
|
||
|
||
def unregister_processor(self, io_type: IOType, processor: Callable) -> None:
|
||
key = io_type.value
|
||
if key in self._processors:
|
||
self._processors[key] = [
|
||
p for p in self._processors[key] if p is not processor
|
||
]
|
||
|
||
def clear_processors(self, io_type: IOType) -> None:
|
||
self._processors.pop(io_type.value, None)
|
||
|
||
# ── 生命周期 ──────────────────────────────────────────────────
|
||
|
||
def start(self) -> None:
|
||
"""启动分发器,连接输入信号"""
|
||
if self._active:
|
||
return
|
||
io_signals.input_received.connect(self._on_input_received)
|
||
self._active = True
|
||
logger.info("✅ IODispatcher 已启动")
|
||
|
||
def stop(self) -> None:
|
||
"""停止分发器"""
|
||
if not self._active:
|
||
return
|
||
io_signals.input_received.disconnect(self._on_input_received)
|
||
self._pool.waitForDone(3000)
|
||
self._active = False
|
||
logger.info("🛑 IODispatcher 已停止")
|
||
|
||
# ── 核心槽函数 ────────────────────────────────────────────────
|
||
|
||
@pyqtSlot(str, str, object)
|
||
def _on_input_received(self, data_id: str, io_type: str, data: object):
|
||
"""
|
||
槽:接收原始输入,提交到线程池处理
|
||
Qt 信号槽队列保证此函数在主线程执行,
|
||
实际处理在 QRunnable 工作线程中进行
|
||
"""
|
||
processors = self._processors.get(io_type, [])
|
||
if not processors:
|
||
# 无处理器,直接透传
|
||
logger.debug(f"无处理器,透传 data_id={data_id[:8]} type={io_type}")
|
||
data.mark_success()
|
||
io_signals.output_ready.emit(data_id, io_type, data)
|
||
return
|
||
|
||
worker = _PipelineWorker(data, processors)
|
||
worker.signals.finished.connect(self._on_worker_finished)
|
||
worker.signals.error.connect(self._on_worker_error)
|
||
self._pool.start(worker)
|
||
logger.debug(f"提交 Worker: data_id={data_id[:8]} type={io_type} "
|
||
f"processors={len(processors)}")
|
||
|
||
@pyqtSlot(object)
|
||
def _on_worker_finished(self, data: object):
|
||
"""槽:Worker 完成,发射 output_ready"""
|
||
io_signals.output_ready.emit(
|
||
data.data_id, data.io_type.value, data
|
||
)
|
||
|
||
@pyqtSlot(str, str)
|
||
def _on_worker_error(self, data_id: str, error_msg: str):
|
||
logger.error(f"Worker 错误 data_id={data_id[:8]}: {error_msg}") |