复制(多个)文件到多个位置

3
使用C#(.NET 4.5),我想将一组文件复制到多个位置(例如将文件夹的内容复制到连接到计算机的2个USB驱动器)。有比仅使用foreach循环和File.Copy更有效的方法吗?
寻找(可能)的解决方案。
我的第一个想法是采用某种多线程方法。经过一些阅读和研究,我发现当涉及到IO时,仅盲目设置类型并/或异步进程不是一个好主意(如为什么Parallel.ForEach要比AsParallel().ForAll()快得多,即使MSDN建议相反?)。
瓶颈在于磁盘,特别是如果它是传统硬盘,因为它只能同步读写。这使我想到,如果我只读取一次,然后在多个位置输出数据,会怎样?毕竟,在我的USB驱动器场景中,我正在处理多个(输出)磁盘。
尽管如此,我仍然无法弄清楚如何做到这一点。我看到的一个想法(从多个线程复制同一文件到多个目标)是仅将每个文件的所有字节读入内存,然后循环遍历目标并将字节写入每个位置,然后再进入下一个文件。如果文件可能很大,那似乎不是一个好主意。我将要复制的一些文件将是视频文件,可能会有1 GB(或更多)。我无法想象将1 GB的文件加载到内存中只为将其复制到另一个磁盘?
因此,为了允许更大的文件灵活性,我最接近的代码如下(基于如何同时将一个文件复制到多个位置)。这段代码的问题在于我仍然没有进行单一读取和多路写入。它目前是多读和多写入。有没有一种方法可以进一步优化这段代码?我能够将块读入内存,然后将该块写入各个目标,然后再进入下一个块(就像上面的想法一样,但是使用分块的文件而不是整个文件)?
files.ForEach(fileDetail =>
    Parallel.ForEach(fileDetail.DestinationPaths, new ParallelOptions(),
        destinationPath =>
        {
            using (var source = new FileStream(fileDetail.SourcePath, FileMode.Open, FileAccess.Read, FileShare.Read))
            using (var destination = new FileStream(destinationPath, FileMode.Create))
            {
                var buffer = new byte[1024];
                int read;

                while ((read = source.Read(buffer, 0, buffer.Length)) > 0)
                {
                    destination.Write(buffer, 0, read);
                }
            }
        }));
2个回答

2

我想发布我的当前解决方案,供其他遇到这个问题的人参考。

如果有人发现更高效/更快的方法,请告诉我!

我的代码似乎比同步复制文件要快一些,但它仍然不如我所希望的快(也不像其他一些程序那样快)。我应该指出,性能可能会因.NET版本和您的系统而异(我正在使用Win 10,.NET 4.5.2,在13英寸的MBP上,配备2.9GHz i5(5287U-2核心/4线程)+ 16GB RAM)。我甚至还没有找到最佳的方法组合(例如,FileStream.WriteFileStream.WriteAsyncBinaryWriter.Write)和缓冲区大小。

foreach (var fileDetail in files)
{
    foreach (var destinationPath in fileDetail.DestinationPaths)
        Directory.CreateDirectory(Path.GetDirectoryName(destinationPath));

    // Set up progress
    FileCopyEntryProgress progress = new FileCopyEntryProgress(fileDetail);

    // Set up the source and outputs
    using (var source = new FileStream(fileDetail.SourcePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, FileOptions.SequentialScan))
    using (var outputs = new CompositeDisposable(fileDetail.DestinationPaths.Select(p => new FileStream(p, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize))))
    {
        // Set up the copy operation
        var buffer = new byte[bufferSize];
        int read;

        // Read the file
        while ((read = source.Read(buffer, 0, buffer.Length)) > 0)
        {
            // Copy to each drive
            await Task.WhenAll(outputs.Select(async destination => await ((FileStream)destination).WriteAsync(buffer, 0, read)));

            // Report progress
            if (onDriveCopyFile != null)
            {
                progress.BytesCopied = read;
                progress.TotalBytesCopied += read;

                onDriveCopyFile.Report(progress);
            }
        }
    }

    if (ct.IsCancellationRequested)
        break;
}

我正在使用来自响应式扩展(https://github.com/Reactive-Extensions/Rx.NET)的CompositeDisposable


你也可以调查 AsyncEnumeratorForEachAsync - VMAtm

1
一般来说,IO操作应该被视为异步操作,因为有些硬件操作是在你的代码之外运行的。因此,你可以尝试引入一些异步/等待构造来进行读写操作,这样你就可以在硬件操作期间继续执行。
while ((read = await source.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
    await destination.WriteAsync(buffer, 0, read);
}

您还必须将lambda委托标记为async,以使其正常工作:

async destinationPath => 
...

你需要一直等待生成的任务。你可以在这里找到更多信息:

使用异步lambda表达式的并行foreach

在Parallel.ForEach中嵌套await


谢谢你的帮助 @VMAtm - 我已经在过去一周里调整了我的代码,并成功使用 Task.WaitAll 实现并行异步写入一次读取。我仍然无法达到我在另一个 (.NET) 程序中看到的速度,但我想知道他们是否在复制时使用了其它东西,因为我在应用程序目录中注意到了 _USBLib.dll_。你了解 IO 缓冲区吗?我发现如果将缓冲区推到更大 (比如 60 MB,而不是我通常的 81920 字节),那么速度会更快,但我不确定这样大的缓冲区是否是一个好主意? - Pete
今天我偶然进行了一些有关缓冲区大小的调查,就我所知,您应该使用更大的缓冲区来衡量性能,有些人说81920是可以的,而其他人则建议使用高达256KB。所以这取决于您,只需测量即可。如果失败,60MB可能会有危险-您可能会失去所有数据。 - VMAtm
1
好的,看起来我只需要对缓冲区大小进行一些测试。感谢您的提示和指引。虽然我几天前就已经找到了解决方案,但我会回复您的答案,因为您确实费心回复并提供了一些有用的见解和链接。 - Pete

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接