从源到目标的同步线程复制

问题描述 投票:0回答:1

我的目标是将大量文件从我的文件服务器复制到 USB 磁盘或其他磁盘以进行备份。对于这个任务,我想编写一个程序,使用线程来复制文件,并让用户知道幕后发生了什么。

这是我写的程序:

import tkinter as tk
from tkinter import filedialog
import datetime
import os
import shutil
import threading
from concurrent.futures import ThreadPoolExecutor

count = 0  # Counts copied files
Ex_dirs_list = []  # Excluded directories - those directories won't be copied.
task_counter = 0  # Counter for tracking the number of tasks
counter_lock = threading.Lock()  # Lock for updating the counter safely

def copy_file(srcfile, dstfile):
    # function to copy file from source to destination
    os.makedirs(os.path.dirname(dstfile), exist_ok=True)
    shutil.copy2(srcfile, dstfile)

def copy_files_parallel(srcfile, dstfile):
    global count
    global task_counter

    try:
        copy_file(srcfile, dstfile)
        return True
    except Exception as e:
        StatusText.insert(tk.INSERT, f"Error copying {srcfile} to {dstfile}: {e}")
        StatusText.see("end")
        return False

def copy_directory(src_dir, dst_dir):
    global count
    global task_counter
    excluded_dirs_set = set(os.path.basename(x) for x in Ex_dirs_list)

    dirs = '\n'.join(Ex_dirs_list)
    StatusText.insert(tk.INSERT, f"Coping files from {src_dir} to {dst_dir}.\nIgnores following directories:\n{dirs}\n\n")
    StatusText.see("end")

    start = datetime.datetime.now()
    with ThreadPoolExecutor() as executor:
        futures = []
        for root, dirs, files in os.walk(src_dir):
            base_dir = os.path.basename(root)
            if base_dir in excluded_dirs_set:
                # Skip excluded directories
                StatusText.insert(tk.INSERT, f"Skipping directory: {base_dir}\n")
                StatusText.see("end")
                continue

            dstpath = os.path.join(dst_dir, root[len(src_dir)+1:])
            for file in files:
                srcfile = os.path.join(root, file)
                dstfile = os.path.join(dstpath, file)
                if All_files.get() == 1:  # If all files checked, copy all the files regardless of file changes
                    StatusText.insert(tk.INSERT, f"{srcfile} => {dstfile}: All files\n")
                    StatusText.see("end")
                    futures.append(executor.submit(copy_files_parallel, srcfile, dstfile))
                else:
                    if os.path.exists(dstfile):
                        if os.path.getsize(srcfile) != os.path.getsize(dstfile):
                            StatusText.insert(tk.INSERT, f"{srcfile} => {dstfile}: changed\n")
                            status_text.see("end")
                            futures.append(executor.submit(copy_files_parallel, srcfile, dstfile))
                    else:
                        StatusText.insert(tk.INSERT, f"{srcfile} => {dstfile}: added\n")
                        StatusText.see("end")
                        futures.append(executor.submit(copy_files_parallel, srcfile, dstfile))

    finish = datetime.datetime.now()
    StatusText.insert(tk.INSERT, f"\nSummary:\n=======\nCopied {count} files\nFinished in {(finish - start).total_seconds()} seconds\n\n")
    StatusText.see("end")

def WinBackUp():
    t = threading.Thread(target=copy_directory, args=(FromDir.get(), ToDir.get()))
    t.start()

def ChooseDir(dir):
    global Excluded_dirs

    if dir == "src":
        FromDir.set(filedialog.askdirectory())
    if dir == "dst":
        ToDir.set(filedialog.askdirectory())
    if dir == "exclude":
        directory = filedialog.askdirectory()
        if directory in Ex_dirs_list:
            StatusText.insert(tk.INSERT, f"Error! Can't choose {directory} more than once\n")
        else:
            Ex_dirs_list.append(directory)
            ExcludeText.delete('1.0',"end")
            ExcludeText.insert(tk.END, "\n".join(Ex_dirs_list))
            Excluded_dirs = [os.path.basename(x) for x in Ex_dirs_list]
    if dir == "delete":
        if len(Ex_dirs_list)>=1:
            directory = filedialog.askdirectory()
            if directory in Ex_dirs_list:
                Ex_dirs_list.remove(directory)
                ExcludeText.delete('1.0',"end")
                ExcludeText.insert(tk.END, "\n".join(Ex_dirs_list))
                Excluded_dirs = [os.path.basename(x) for x in Ex_dirs_list]
            else:
                StatusText.insert(tk.INSERT, f"Error! directory {directory} is not in list\n")
        else:
            StatusText.insert(tk.INSERT, f"Error! no directory chosen\n")

win = tk.Tk()
win.geometry("910x350")

ToDir = tk.StringVar()
FromDir = tk.StringVar()
All_files = tk.IntVar()
All_files.set(0)  # Default: copy only the changed files

TilteLabel = tk.Label(win, text="Smart Backup", font=("Arial", 14)).pack()
FromButton = tk.Button(win, text="From", command=lambda:ChooseDir("src")).place(x=10, y=50)
FromEntry = tk.Entry(win, textvariable=FromDir, width=10)
FromEntry.place(x=60, y=50, height=25)

ToButton = tk.Button(win, text="To", command=lambda: ChooseDir("dst")).place(x=150, y=50)
ToEntry = tk.Entry(win, textvariable=ToDir, width=10)
ToEntry.place(x=180, y=50, height=25)

ExcludeButton = tk.Button(win, text="Excluded folders / files", command=lambda: ChooseDir("exclude")).place(x=10, y=90)
ExcludeText = tk.Text(win)
ExcludeText.place(x=10, y=120, height=155, width=250)

DelDirButton = tk.Button(win, text="Del dir", command=lambda: ChooseDir("delete")).place(x=200, y=90)

StatusLabel = tk.Label(win, text="Status")
StatusText = tk.Text(win)
scroll_bar = tk.Scrollbar(win)
scroll_bar.pack(side=tk.RIGHT)
StatusText.place(x=290, y=70, height=205, width=600)
StatusLabel.place(x=290, y=45)

IsAllFiles = tk.Checkbutton(win, text="All files", variable=All_files)
IsAllFiles.place(x=100, y=300)
BackupButton = tk.Button(win, text="Backup", command=WinBackUp).place(x=200, y=300)

win.mainloop()

因此,程序会复制文件,但更新不是实时的,例如,我在文件复制的文本小部件中看到更新,但该文件在目标中尚不存在。

据我了解,操作系统将管理线程,因此我无法知道会同时复制多少个文件。如果比逐一复制快的话对我有好处(我可以检查一下吗?)

如何将实时更新与当前发生的操作同步,以便当前正在处理的文件将在发生的同时进行更新?

python multithreading tkinter synchronization
1个回答
0
投票

文件复制应用程序中的实时更新意味着将 UI 更新与文件复制任务的完成同步。

+---------------------------------------+
| tkinter GUI                           |
| +-------------------+  +------------+ |
| | File Selection    |  | Status Box | |
| +-------------------+  +------------+ |
|                                       |
| [FromDir] --> [copy_directory]        |
|                 |                     |
|                 v                     |
|            [ThreadPoolExecutor]       |
|                 |                     |
|                 v                     |
|          [copy_files_parallel]        |
|                 |                     |
|    [Real-time status updates in GUI]  |
+---------------------------------------+

您当前的实现是异步的:文件复制操作被提交到线程池,并且无需等待这些操作完成即可更新UI。因此,文件复制操作和 UI 更新之间的时间存在差异。

不要直接从

copy_files_parallel
函数将文本插入到状态框中,而是使用 回调机制 仅在文件成功复制后更新 UI。

由于 Tkinter 不是线程安全的,因此您应该在主线程中安排 UI 更新。这可以使用

tkinter.Tk.after
方法 来完成,如此处所示

update_status
函数更改为一个简单的包装器,使用
tk.after
安排实际的 UI 更新。文件复制操作完成后,使用
tk.after
安排 Tkinter 主线程中的更新。

def copy_files_parallel(srcfile, dstfile):
    try:
        copy_file(srcfile, dstfile)
        win.after(0, update_status, f"Successfully copied {srcfile} to {dstfile}")
        return True
    except Exception as e:
        win.after(0, update_status, f"Error copying {srcfile} to {dstfile}: {e}")
        return False

def update_status(message):
    StatusText.insert(tk.INSERT, message + '\n')
    StatusText.see("end")

copy_directory
函数变为:

# Other parts of your function 

with ThreadPoolExecutor() as executor:
    futures = []
    for root, dirs, files in os.walk(src_dir):
        # Directory and file processing logic 

        for file in files:
            # File handling logic 
            futures.append(executor.submit(copy_files_parallel, srcfile, dstfile))

# Rest of your function 

提交到线程池的每个文件复制任务将在复制操作完成后通过

tk.after
独立调度自己的 UI 更新。这应该确保所有 UI 更新都发生在主线程中,从而维护 Tkinter 的线程安全。

尝试复制大小文件的混合并观察状态更新。
注意:您还可以实时显示进度,例如进度条或复制的文件总数。

© www.soinside.com 2019 - 2024. All rights reserved.