Асинхронное копирование каталога с индикатором выполнения

В настоящее время я создаю приложение, которое позволяет мне выбрать каталог и скопировать его в 4 разных места в зависимости от того, что вы выбрали

Изображение приложения

И момент, когда он копирует основной файл for, блокируется, поэтому я хочу, чтобы копии каталогов работали в разных потоках, чтобы это не происходило. Кроме того, мне нужно связать индикаторы выполнения для каждой копии каталога, чтобы показать ход передачи.

Я немного заржавел, когда дело доходит до 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");
        }
    }

1 ответ

Решение

Ваш вопрос имеет несколько проблем:

  • Как скопировать каталоги с помощью async- await
  • Как показать прогресс в вашем диалоговом окне
  • Является ли выбранный метод эффективным?

Как скопировать каталоги с помощью async- await

Каждая функция, которая использует async- await, должна возвращать Task вместо void а также Task<TResult> вместо TResult, Есть одно исключение: асинхронный обработчик событий. Обработчик события возвращает void.

MSDN об асинхронном файловом вводе / выводе поставляется со следующим:

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);
}

Обратная связь в вашем интерфейсе

Если вам достаточно обновить индикатор выполнения для каждого скопированного файла, попробуйте позволить 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);
            } 
        } 
    }
}

Во всех моих примерах я использую чистый async- await без запуска нового потока. Участвует только один поток (или, если быть более точным: только один поток за один раз). Как это может сделать ваш процесс быстрее?

В этом интервью Эрик Липперт сравнил асинхронное ожидание с командой поваров, которые должны готовить ужин (где-то в середине статьи ищите асинхронное ожидание).

По аналогии с Эриком Липпертом, если повару придется ждать, пока хлеб поджарится, он не будет лениво ничего не делать. Вместо этого он оглядывается, чтобы увидеть, может ли он сделать что-то еще. Через некоторое время, когда хлеб поджарен, он продолжает обрабатывать поджаренный хлеб или в команде поваров: один из его коллег обрабатывает поджаренный хлеб.

То же самое в асинхронном ожидании. Ваш поток может делать только одну вещь за один раз, и пока он занят, он не может делать больше ничего. Во время копирования каталога несколько раз ваш поток будет ожидать, а именно во время чтения исходного файла и во время записи файла назначения. Так что, если вы скажете вашему потоку сделать что-то другое вместо ожидания, это может ускорить процесс.

Таким образом, на первый взгляд кажется, что async- await поможет вам: ожидая записи первого файла, ваш поток может начать читать второй файл. Увы, устройство, которое записывает ваш файл, вероятно, будет таким же, как и устройство, которое читает ваши файлы, и, таким образом, ваше устройство будет слишком загружено, чтобы обработать ваш запрос на чтение второго файла. Поэтому я не уверен, будет ли ваш процесс намного быстрее, если вы используете async- await.

То же самое, если вы действительно запускаете несколько потоков. Это будет только быстрее, если запись на разные устройства.

Если вы хотите, чтобы ваш пользовательский интерфейс был отзывчивым, рассмотрите возможность использования класса BackGroundWorker. Легче запускать и останавливать, а также легче сообщать о прогрессе.

Другие вопросы по тегам