带进度条的异步目录复制

Asynchronous Directory Copy with Progress Bar

我目前正在创建一个应用程序,允许我 select 一个目录并将其复制到最多 4 个不同的位置,具体取决于您 selected

Image of app

当它复制 main for 锁时,我想在不同线程上获取目录副本 运行ning 以阻止这种情况发生。最重要的是,我需要 link 提高每个目录副本的进度条以显示传输进度。

我对 C# 有点生疏,如果有人能为我指出正确的方向,我会很高兴。我尝试使用 await/async 但它仍然只是 运行 每个目录不是在同一时间相互复制。

     private void button1_Click(object sender, EventArgs e)
    {
        //
        // This event handler was created by double-clicking the window in the designer.
        // It runs on the program's startup routine.
        //
        Path.GetFileName(folderBrowserDialog1.SelectedPath);
        folderBrowserDialog1.RootFolder = Environment.SpecialFolder.MyComputer;
        folderBrowserDialog1.SelectedPath = @"C:\Delivery";
        DialogResult result = folderBrowserDialog1.ShowDialog();
        if (result == DialogResult.OK)
        {
            //
            // The user selected a folder and pressed the OK button.
            // We print the number of files found.
            //
            //  string[] files = Directory.GetFiles(folderBrowserDialog1.SelectedPath);
            //  MessageBox.Show("Files found: " + files.Length.ToString(), "Message");

            if (checkBox1.Checked == true)
            {

                DirectoryCopy(folderBrowserDialog1.SelectedPath, "C:\temp\1\" + Path.GetFileName(folderBrowserDialog1.SelectedPath), true);

            }
            if (checkBox2.Checked == true)
            {

                 DirectoryCopy(folderBrowserDialog1.SelectedPath, "C:\temp\2\" + Path.GetFileName(folderBrowserDialog1.SelectedPath), true);
            }
            if (checkBox3.Checked == true)
            {

                 DirectoryCopy(folderBrowserDialog1.SelectedPath, "C:\temp\3\" + Path.GetFileName(folderBrowserDialog1.SelectedPath), true);
            }
            if (checkBox4.Checked == true)
            {

                 DirectoryCopy(folderBrowserDialog1.SelectedPath, "C:\temp\4\" + Path.GetFileName(folderBrowserDialog1.SelectedPath), true);
            }
            if (checkBox5.Checked == true)
            {

                 DirectoryCopy(folderBrowserDialog1.SelectedPath, "C:\temp\5\"+ Path.GetFileName(folderBrowserDialog1.SelectedPath), true);
            }
            MessageBox.Show("These folders have been successfully transfered");
        }
    }

你的问题有几个问题:

  • 如何使用 async-await 复制目录
  • 如何在对话框中显示进度
  • 选择的方法是否有效?

如何使用 async-await 复制目录

每个使用 async-await 的函数都必须 return Task 而不是 voidTask<TResult> 而不是 TResult。有一个例外:异步事件处理程序。事件处理程序 return 无效。

MSDN about Asynchronous File I/O 附带以下内容:

public class FileCopier
{
    public System.IO.FileInfo SourceFile {get; set;}
    public System.IO.DirectoryInfo DestinationFolder {get; set;}

    public async Task CopyAsync()
    {
        // TODO: throw exceptions if SourceFile / DestinationFile null
        // TODO: throw exception if SourceFile does not exist
        string destinationFileName = Path.Combine(DestinationFoler.Name, SourceFile.Name);
        // TODO: decide what to do if destinationFile already exists

         // open source file for reading
         using (Stream sourceStream = File.Open(SourceFile, FileMode.Open))
         {
             // create destination file write
             using (Stream destinationStream = File.Open(DestinationFile, FileMode.CreateNew))
             {
                 await CopyAsync(sourceStream, destinationStream);
             }
        }

        public async Task CopyAsync(Stream Source, Stream Destination) 
        { 
            char[] buffer = new char[0x1000]; 
            int numRead; 
            while ((numRead = await Source.ReadAsync(buffer, 0, buffer.Length)) != 0) 
            {
                await Destination.WriteAsync(buffer, 0, numRead);
            } 
        } 
    }
}

你的目录复制器可能是这样的:

class FolderCopier
{
    public System.IO.DirectoryInfo SourceFolder {get; set;}
    public System.IO.DirectoryInfo DestinationFolder {get; set;}

    public Task CopyAsync()
    {
        foreach (FileInfo sourceFile in SourceFolder.EnumerateFiles())
        {
            var fileCopier = new FileCopier()
            {
                Sourcefile = sourceFile,
                DestinationFolder = this.DestinationFolder,
            };
            await fileCopier.CopyAsync();
        }
    }
}

最后是您的事件处理程序:

private async void OnButtonDeploy_Clicked(object sender, ...)
{
    DirectoryInfo sourceFolder = GetSourceFolder(...);
    IEnumerable<DirectoryInfo> destinationFolders = GetDestinationFolders();
    IEnumerable<DirectoryCopier> folderCopiers = destinationFolders
        .Select(destinationFolder => new FolderCopier()
        {
            SourceFolder = sourceFolder,
            DestinationFolder = destinationFolder,
        });

    // if you want to copy the folders one after another while still keeping your UI responsive:
    foreach (var folderCopier in folderCopiers)
    {
        await folderCopier.CopyAsync();
    }

    // or if you want to start copying all:
    List<Task> folderCopyTasks = new List<Task>();
    foreach (var folderCopier in folderCopiers)
    {
        folderCopyTasks.Add(folderCopier.CopyAsync());
    }
    await Task.WhenAll(folderCopyTasks);
}

在您的UI

中反馈

如果为每个复制的文件更新进度条就足够了,请考虑让 FolderCopier 在复制文件时引发事件。

public class FileCopiedEventArgs
{
    public string DestinationFolder {get; set;}
    public int NrOfFilesCopied {get; set;}
    public int NrOfFilesToCopy {get; set;}
}

class FolderCopier
{
    public System.IO.DirectoryInfo SourceFolder {get; set;}
    public System.IO.DirectoryInfo DestinationFolder {get; set;}

    public Task CopyAsync()
    {
        List<FileInfo> filesToCopy = DestinationFolder.EnumerateFiles().ToList();

        for (int i=0; i<filesToCopy.Count; ++i)
        {
            // copy one file as mentioned before
            // notify listeners:
            this.OnFileCopied(i, filesToCopy.Count);
        }

        public event EventHandler<FileCopyiedEventArgs> EventFileCopied;
        public void OnFileCopied(int nrOfCopiedFiles, int filesToCopy)
        {
            var tmpEvent = this.EventFileCopied;
            if (tmpEvent != null)
            {
                tmpEvent.Invoke(this, new FileCopiedEventArgs()
                {
                    DestinationFolder = this.DestinationFolder.Name,
                    NrOfFilesCopied = nrOfCopiedFiles,
                    NrOfFilesToCopy = filesToCopy
                });
            }

            // instead of checking for null you can use the null-coalescent operator:
            this.EventFileCopied?.Invocke(this, new ...);
        }
    }
}

正在注册这些活动:

private async void OnButtonDeploy_Clicked(object sender, ...)
{
    var folderCopier = new FolderCopier(...);
    folderCopier.EventFileCopied += eventFileCopied;
}

private void EventFileCopied(object sender, ...)
{
    // for you to solve: if one copier has already copied 10%
    // and another copier only 2%, what value should the progress bar have?
}

文件复制时的效率

您想将每个源文件复制到多个位置。如果您在单独的进程中执行此操作,则每个目标文件将读取一次源文件。读取一次源文件并将它们写入所有目标文件似乎效率更高。

class MultiFileCopier
{
    public System.IO.FileInfo SourceFile {get; set;}
    public IEnumerable<System.IO.FileInfo> DestinationFiles {get; set;}

    public async Task CopyAsync(object sender, RoutedEventArgs e)
    {
        // open your source file as a streamreader
        using (Stream sourceStream = File.Open(SourceFile, Open))
        {
            // open all destination files for writing:
            IEnumerable<Stream> destinationStreams = this.OpenDestinationsForWriting();
            await CopyFilesAsync(sourceStream, destinationStreams);
            this.DisposeDestinationStreams(destinationStreams);
         }
    }
    public async Task CopyAsync(Stream Source, IEnumerable<Stream> destinations) 
        { 
            char[] buffer = new char[0x1000]; 
            int numRead; 
            while ((numRead = await Source.ReadAsync(buffer, 0, buffer.Length)) != 0) 
            {
                // write one after another:
                foreach (var destination in destinations)
                {
                    await Destination.WriteAsync(buffer, 0, numRead);
                }
                // or write to them all at once:
                List<Task> writeTasks = new List<Task>();
                foreach (var destination in destinations)
                {
                    writeTasks.Add(Destination.WriteAsync(buffer, 0, numRead));
                }
                await Task.WhenAll(writeTasks);
            } 
        } 
    }
}

在我的所有示例中,我都使用纯异步等待而不启动新线程。只涉及一个线程(或者更准确地说:一次只涉及一个线程)。这如何使您的流程更快?

this interview 中,Eric Lippert 将 async await 与一组必须准备晚餐的厨师进行了比较(在文章中间的某个地方搜索 async-await)。

用 Eric Lippert 的比喻,如果厨师必须等待面包烤好,他就不会懒惰地什么都不做。相反,他环顾四周,看看是否可以做点别的事情。面包烤了一会儿后,他继续加工烤面包,或者在一组厨师中:他的一位同事加工烤面包。

async-await也是一样。你的线程一次只能做一件事,只要他忙,他就不能做其他事情。在目录复制期间,您的线程会多次等待,即在读取源文件期间和在写入目标文件期间。因此,如果您告诉您的线程做其他事情而不是等待,它可能会加快进程。

所以乍一看,async-await 似乎可以帮助您:在等待写入第一个文件时,您的线程可以开始读取您的第二个文件。 las,写入文件的设备可能与读取文件的设备相同,因此您的设备将太忙而无法处理读取第二个文件的请求。所以我不确定如果你使用 async-await,你的过程是否会更快。

如果真的开启了几个线程也是一样的。只有写入不同的设备才会更快。

如果您只想保持 UI 响应,请考虑使用 BackGroundWorker class。更容易开始和停止,更容易报告进度。