如何在复制文件时';It’写得尽可能快

本文关键字:It 尽可能 复制 文件 | 更新日期: 2023-09-27 18:20:36

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. 所有这些都必须以我所能达到的最快速度进行

我可以很容易地处理同步问题(我使用CopyFileEx的CopyProgressRoutine回调来知道完成了多少字节,并相应地触发事件),但文件在复制时被锁定以供读取。我不能使用普通C#的FileStream——它太慢了。。。

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

  • 卷影复制(特别是AlphaVSS)
  • 内存映射文件-我设法做得很快,但我担心系统实际上使用了缓存,并且没有真正从MID读取
  • 一些我不知道的win-API P/Invoke函数

如何在复制文件时';It’写得尽可能快

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

基本思想是打开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,或者您可以在WaitOne上使用超时,如下所示:

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