【问题标题】:How to copy a file while it's being written as fast as possible?如何在尽可能快地写入文件时复制文件?
【发布时间】:2013-07-08 15:18:06
【问题描述】:

TL/DR:

我有两台机器:A 和 B。我制作了一个测试程序,以测试它们之间的介质(接口) - 我在将文件从 A 复制到 B 再从 B 复制到 A 时检查错误,但我必须这样做我能做到的最快。所以我有一个源文件:SRC,我将它复制到 B 到新文件:MID,然后我再次将 MID 从 B 复制到 A 到新文件 DST,然后我将 SRC 与 DST 进行比较。这里的问题是如何以尽可能高的速度(即并行)来完成

详细说明:

如何在写入的同时复制文件?我使用CopyFileEx 将文件从SRC 复制到MID,同时我必须再次将它从MID 复制到DST。数据必须显式通过磁盘,我不能使用内存缓冲区或缓存,并且:

  1. 必须在 MID 上创建文件时执行第二次复制 - 我不能等待它完成复制。
  2. 我必须再次明确地从 MID 读取文件 - 我不能使用我用来从 SRC 复制到 MID 的缓冲区
  3. 这一切都必须以我能做到的最快速度完成

我可以轻松处理同步问题(我使用CopyFileExCopyProgressRoutine 回调来了解完成了多少字节并相应地触发事件),但是文件在被复制时被锁定以供读取。我不能使用普通 C# 的 FileStream - 太慢了...

我目前正在研究的可能解决方案:

  • 卷影复制(特别是AlphaVSS
  • memory-mapped-file - 我设法做到了非常快,但我担心系统实际上使用缓存,并没有真正从 MID 读回
  • 一些我不知道的 win-API P/Invoke 函数??

【问题讨论】:

  • FileStream 只是 WIN API 文件函数的一个薄包装,所以如果这太慢了,我不确定什么会更快......
  • @MatthewWatson:也许读取文件到内存和从内存写入磁盘本来就很慢,并且使用 winapi 来做到这一点不会比托管的FileStream 快。我怀疑这是真的,这就是为什么我不倾向于朝那个方向发展。我使用winapi的CopyFileEx,它与使用Windows界面复制文件一样快(C#的File.Copy具有相同的速度,但本机CopyFileEx也提供了管理File.Copy缺乏的“进度回调”,我使用这个回调来并行化事情- 我不能做“c#-way”)
  • 您为什么不 1) 使用 FileStream 打开输入文件,2) 使用两个 FileStream 同时写入两个输出?与 Stream.CopyTo 类似,但具有多个具有相同缓冲区的输出流,而不是只有一个输出流。每个输出都可以在不同的线程/任务上完成,但如果硬盘相同,则不确定它会改变什么。
  • @SimonMourier:因为我希望 SRC 复制到 MID,MID 复制到 DST,而不是 SRC 复制到 MID 和 DST - 这里的重点是 MID 复制到 DST:SRC -> MID -> 夏令时
  • SRC->MID & SRC-> DST 和 SRC->MID->DST 有什么区别?

标签: c# winapi pinvoke memory-mapped-files volume-shadow-service


【解决方案1】:

为了能够在写入文件时读取文件,它必须是使用dwShareMode = FILE_SHARE_READ 创建的。您可能不得不放弃CopyFileEx 并使用CreateFile/ReadFile/WriteFile 自己实现它。对于异步读/写,您可以使用ReadFile/WriteFile 函数的lpOverlapped 参数。

【讨论】:

    【解决方案2】:

    基本思路是打开MID文件进行读写。简单的单线程方法是:

    private static void FunkyCopy(string srcFname, string midFname, string dstFname)
    {
        using (FileStream srcFile = new FileStream(srcFname, FileMode.Open, FileAccess.Read, FileShare.None),
                            midFile = new FileStream(midFname, FileMode.Create, FileAccess.ReadWrite,
                                                    FileShare.ReadWrite),
                            dstFile = new FileStream(dstFname, FileMode.Create, FileAccess.Write, FileShare.None))
        {
            long totalBytes = 0;
            var buffer = new byte[65536];
            while (totalBytes < srcFile.Length)
            {
                var srcBytesRead = srcFile.Read(buffer, 0, buffer.Length);
                if (srcBytesRead > 0)
                {
                    // write to the mid file
                    midFile.Write(buffer, 0, srcBytesRead);
                    // now read from mid and write to dst
                    midFile.Position = totalBytes;
                    var midBytesRead = midFile.Read(buffer, 0, srcBytesRead);
                    if (midBytesRead != srcBytesRead)
                    {
                        throw new ApplicationException("Error reading Mid file!");
                    }
                    dstFile.Write(buffer, 0, srcBytesRead);
                }
                totalBytes += srcBytesRead;
            }
        }
    }
    

    正如您所指出的,这将非常缓慢。您可以通过创建两个线程来加快速度:一个用于执行 SRC -> MID 复制,另一个用于执行 MID -> DST 复制。它涉及更多一点,但不是很复杂。

    static void FunkyCopy2(string srcFname, string midFname, string dstFname)
    {
        var cancel = new CancellationTokenSource();
        const int bufferSize = 65536;
    
        var finfo = new FileInfo(srcFname);
        Console.WriteLine("File length = {0:N0} bytes", finfo.Length);
        long bytesCopiedToMid = 0;
        AutoResetEvent bytesAvailable = new AutoResetEvent(false);
    
        // First thread copies from src to mid
        var midThread = new Thread(() =>
            {
                Console.WriteLine("midThread started");
                using (
                    FileStream srcFile = new FileStream(srcFname, FileMode.Open, FileAccess.Read, FileShare.None),
                                midFile = new FileStream(midFname, FileMode.Create, FileAccess.Read,
                                                        FileShare.ReadWrite))
                {
                    var buffer = new byte[bufferSize];
                    while (bytesCopiedToMid < finfo.Length)
                    {
                        var srcBytesRead = srcFile.Read(buffer, 0, buffer.Length);
                        if (srcBytesRead > 0)
                        {
                            midFile.Write(buffer, 0, srcBytesRead);
                            Interlocked.Add(ref bytesCopiedToMid, srcBytesRead);
                            bytesAvailable.Set();
                        }
                    }
                }
                Console.WriteLine("midThread exit");
            });
    
        // Second thread copies from mid to dst
        var dstThread = new Thread(() =>
            {
                Console.WriteLine("dstThread started");
                using (
                    FileStream midFile = new FileStream(midFname, FileMode.Open, FileAccess.Read,
                                                        FileShare.ReadWrite),
                                dstFile = new FileStream(dstFname, FileMode.Create, FileAccess.Write, FileShare.Write)
                    )
                {
                    long bytesCopiedToDst = 0;
                    var buffer = new byte[bufferSize];
                    while (bytesCopiedToDst != finfo.Length)
                    {
                        // if we've already copied everything from mid, then wait for more.
                        if (Interlocked.CompareExchange(ref bytesCopiedToMid, bytesCopiedToDst, bytesCopiedToDst) ==
                            bytesCopiedToDst)
                        {
                            bytesAvailable.WaitOne();
                        }
                        var midBytesRead = midFile.Read(buffer, 0, buffer.Length);
                        if (midBytesRead > 0)
                        {
                            dstFile.Write(buffer, 0, midBytesRead);
                            bytesCopiedToDst += midBytesRead;
                            Console.WriteLine("{0:N0} bytes copied to destination", bytesCopiedToDst);
                        }
                    }
                }
                Console.WriteLine("dstThread exit");
            });
    
        midThread.Start();
        dstThread.Start();
    
        midThread.Join();
        dstThread.Join();
        Console.WriteLine("Done!");
    }
    

    这会大大加快速度,因为第二个线程中的读取和写入可以在很大程度上与第一个线程中的读取和写入重叠。最有可能的是,您的限制因素将是存储 MID 的磁盘的速度。

    您可以通过执行异步写入来提高速度。也就是说,让线程读取缓冲区,然后触发异步写入。在执行该写入时,正在读取下一个缓冲区。请记住在该线程中开始另一个异步写入之前等待异步写入完成。所以每个线程看起来像:

    while (bytes left to copy)
        Read buffer
        wait for previous write to finish
        write buffer
    end while
    

    我不知道这会给您带来多大的性能提升,因为您在对 MID 文件的并发访问方面受到限制。但这可能值得一试。

    我知道那里的同步代码会阻止第二个线程在不应该读取时尝试读取。我认为它将防止第二个线程锁定的情况,因为它在第一个线程退出后等待信号。如果有任何疑问,您可以使用 ManualResetEvent 来表示第一个线程已完成,然后使用 WaitHandle.WaitAny 等待它和 AutoResetEvent,或者您可以在 @ 上使用超时987654327@,像这样:

    bytesAvailable.WaitOne(1000); // waits a second before trying again
    

    【讨论】:

    • 感谢您的努力!我注意到您使用了 midFile.Position = totalBytes; - 它是 midFile.Flush(); 的替代品吗?无论如何,不​​幸的是这很慢,我试过了(只是使用“立即刷新”选项或明确的midFile.Flush();)。
    • @Tal:这样设置位置就相当于寻找。它实际上可能不会导致文件系统将其缓冲区刷新到设备。文件系统可以为来自其内部缓冲区的读取请求提供服务。如果要确保从设备处理请求,则需要刷新。如果这样做会很慢,因为您将程序的速度限制为设备的速度。尽管如此,对于异步写入,复制的总时间应该少于按顺序复制两个文件的时间。
    • 不幸的是,事实并非如此。刷新,即使是大块(10MB),速度也会比普通复制慢 20 倍!这对我来说是个谜,为什么会有如此大的差异。
    猜你喜欢
    • 1970-01-01
    • 2013-10-23
    • 1970-01-01
    • 1970-01-01
    • 2020-07-04
    • 2016-07-25
    • 2014-07-18
    • 1970-01-01
    • 2011-08-31
    相关资源
    最近更新 更多