如何在尽可能快地写入文件时复制文件?

时间:2013-07-02 06:31:37

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

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. 所有这些必须以最快的速度执行
  4. 我可以轻松处理同步问题(我使用CopyFileExCopyProgressRoutine回调来知道完成了多少字节并相应地触发事件),但文件被锁定以便在复制时进行读取。我不能使用普通的C#FileStream - 这太慢了......

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

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

2 个答案:

答案 0 :(得分:1)

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

答案 1 :(得分:1)

基本思想是打开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 - &gt; MID副本,另一个用于执行MID - &gt; 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,或者您可以在WaitOne上使用超时,如下所示:

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