PySide2异步多线程文件复制任务实现及暂停方案咨询
技术建议:文件复制UI的线程实现优化
一、当前QThread+asyncio方案的问题分析
- 代码语法错误:
self.running = False缩进错误,应放在main_process函数内部,否则会触发运行时错误。修正后的代码如下:
class CopyThread(QThread): def run(self) -> None: self.running = True asyncio.run(self.main_process(unix_local_path, unix_remote_path)) async def main_process(self, local_path, remote_path): async with aiofiles.open(PATH_LIST_FILE, 'r+', encoding='utf-8') as file_list_io: async for line in file_list_io: line = line.strip() if not line: continue remote_file_path = line local_file_path = remote_file_path.replace(remote_path, local_path) if not self.running: # 用if替代assert,避免release模式下断言被移除 break await aioshutil.copy2(local_file_path, remote_file_path) self.running = False # 修正缩进至函数内部
- 性能不如多线程的原因:磁盘IO属于阻塞型IO,asyncio的异步文件操作并没有突破磁盘硬件的IO瓶颈,反而因为异步调度的额外开销,在批量小文件复制场景下可能比多线程慢。多线程在IO等待时会释放GIL,能更高效利用CPU空闲时间处理其他复制任务。
二、多线程实现文件复制+暂停功能的正确方案
核心思路
- 用
QThread作为线程载体,内部维护运行标志(控制停止)和暂停事件(控制暂停/恢复)。 - 每个文件复制任务执行前,先检查暂停事件:若处于暂停状态则阻塞等待恢复信号;同时检查运行标志,若已停止则直接退出线程。
- 用标准库
shutil.copy2完成文件复制,磁盘IO阻塞时多线程能自然利用IO等待时间。
完整实现代码
from PyQt5.QtCore import QThread, pyqtSignal import threading import shutil import os class CopyThread(QThread): # 自定义信号:传递当前复制进度(已复制文件数/总文件数) progress_updated = pyqtSignal(int, int) # 自定义信号:传递复制错误信息 copy_error = pyqtSignal(str) def __init__(self, local_path, remote_path, path_list_file): super().__init__() self.local_path = local_path self.remote_path = remote_path self.path_list_file = path_list_file # 运行标志:控制线程是否继续执行 self._running = False # 暂停事件:控制暂停/恢复,初始为非暂停状态 self._pause_event = threading.Event() self._pause_event.set() def start_copy(self): self._running = True self._pause_event.set() # 确保启动时是非暂停状态 self.start() def stop_copy(self): self._running = False self._pause_event.set() # 唤醒暂停等待的线程,使其能及时退出 def pause_copy(self): self._pause_event.clear() def resume_copy(self): self._pause_event.set() def run(self): try: # 预读取所有文件路径,统计总数用于进度显示 with open(self.path_list_file, 'r', encoding='utf-8') as f: file_paths = [line.strip() for line in f if line.strip()] total_files = len(file_paths) copied_count = 0 for remote_file_path in file_paths: # 检查是否需要停止 if not self._running: break # 检查是否需要暂停:事件未设置则阻塞等待 self._pause_event.wait() # 暂停期间可能收到停止信号,再次检查 if not self._running: break local_file_path = remote_file_path.replace(self.remote_path, self.local_path) # 确保本地文件存在 if not os.path.exists(local_file_path): self.copy_error.emit(f"本地文件不存在:{local_file_path}") continue try: shutil.copy2(local_file_path, remote_file_path) copied_count += 1 self.progress_updated.emit(copied_count, total_files) except Exception as e: self.copy_error.emit(f"复制失败 {remote_file_path}:{str(e)}") except Exception as e: self.copy_error.emit(f"读取文件列表失败:{str(e)}") finally: self._running = False
关键细节说明
- 暂停/恢复机制:通过
threading.Event的wait()方法实现阻塞等待,调用pause_copy()时清除事件,线程会在wait()处阻塞;调用resume_copy()时设置事件,线程继续执行。 - 停止机制:停止时不仅设置
_running为False,还要调用_pause_event.set(),确保线程从暂停等待中唤醒,及时检查停止标志并退出。 - 信号传递:用PyQt自定义信号将进度、错误信息传递到UI线程,避免子线程直接操作UI(PyQt要求UI操作必须在主线程)。
- 错误处理:增加文件存在性检查和复制异常捕获,避免单个文件复制失败导致整个线程崩溃。
三、UI端使用示例
# 在UI类中初始化线程 self.copy_thread = CopyThread(local_path, remote_path, PATH_LIST_FILE) self.copy_thread.progress_updated.connect(self.update_progress_bar) self.copy_thread.copy_error.connect(self.show_error_message) # 绑定按钮事件 self.start_btn.clicked.connect(self.copy_thread.start_copy) self.pause_btn.clicked.connect(self.copy_thread.pause_copy) self.resume_btn.clicked.connect(self.copy_thread.resume_copy) self.stop_btn.clicked.connect(self.copy_thread.stop_copy)
内容的提问来源于stack exchange,提问作者temp_one
相关产品推荐
相关产品推荐

