【问题标题】:Writing To A File With Multiple Streams C#写入具有多个流 C# 的文件
【发布时间】:2015-07-31 17:37:00
【问题描述】:

我正在尝试使用 HTTP 将一个大文件 (>1GB) 从一台服务器下载到另一台服务器。为此,我正在并行发出 HTTP 范围请求。这让我可以并行下载文件。

保存到磁盘时,我会获取每个响应流,将同一个文件作为文件流打开,寻找我想要的范围然后写入。

但是,我发现除了一个响应流之外的所有响应流都会超时。 看起来磁盘 I/O 跟不上网络 I/O。但是,如果我做同样的事情,但让每个线程写入一个单独的文件,它就可以正常工作。

作为参考,这是我写入同一文件的代码:

int numberOfStreams = 4;
List<Tuple<int, int>> ranges = new List<Tuple<int, int>>();
string fileName = @"C:\MyCoolFile.txt";
//List populated here
Parallel.For(0, numberOfStreams, (index, state) =>
{
    try
    {
        HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create("Some URL");
        using(Stream responseStream = webRequest.GetResponse().GetResponseStream())
        {
            using (FileStream fileStream = File.Open(fileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write))
            {
                fileStream.Seek(ranges[index].Item1, SeekOrigin.Begin);
                byte[] buffer = new byte[64 * 1024];
                int bytesRead;
                while ((bytesRead = responseStream.Read(buffer, 0, buffer.Length)) > 0)
                {
                    if (state.IsStopped)
                    {
                        return;
                    }
                    fileStream.Write(buffer, 0, bytesRead);
                }
            }
        };
    }
    catch (Exception e)
    {
        exception = e;
        state.Stop();
    }
});

这是写入多个文件的代码:

int numberOfStreams = 4;
List<Tuple<int, int>> ranges = new List<Tuple<int, int>>();
string fileName = @"C:\MyCoolFile.txt";
//List populated here
Parallel.For(0, numberOfStreams, (index, state) =>
{
    try
    {
        HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create("Some URL");
        using(Stream responseStream = webRequest.GetResponse().GetResponseStream())
        {
            using (FileStream fileStream = File.Open(fileName + "." + index + ".tmp", FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write))
            {
                fileStream.Seek(ranges[index].Item1, SeekOrigin.Begin);
                byte[] buffer = new byte[64 * 1024];
                int bytesRead;
                while ((bytesRead = responseStream.Read(buffer, 0, buffer.Length)) > 0)
                {
                    if (state.IsStopped)
                    {
                        return;
                    }
                    fileStream.Write(buffer, 0, bytesRead);
                }
            }
        };
    }
    catch (Exception e)
    {
        exception = e;
        state.Stop();
    }
});

我的问题是,C#/Windows 在从多个线程写入单个文件时是否会采取一些额外的检查/操作,这会导致文件 I/O 比写入多个文件时慢?所有磁盘操作都应该受磁盘速度的约束吗?谁能解释这种行为?

提前致谢!

更新:这是源服务器抛出的错误:

“无法将数据写入传输连接:连接尝试失败,因为连接方在一段时间后没有正确响应,或者建立连接失败,因为连接的主机没有响应。” [System.IO.IOException]:“无法将数据写入传输连接:连接尝试失败,因为连接方在一段时间后没有正确响应,或者连接失败,因为连接的主机没有响应。” InnerException:“连接尝试失败,因为连接方在一段时间后没有正确响应,或者建立连接失败,因为连接主机没有响应” 消息:“无法将数据写入传输连接:连接尝试失败,因为连接方在一段时间后没有正确响应,或者建立连接失败,因为连接的主机没有响应。” StackTrace: " 在 System.Net.Sockets.NetworkStream.Write(Byte[] 缓冲区,Int32 偏移量,Int32 大小)\r\n 在 System.Net.Security._SslStream.StartWriting(Byte[] 缓冲区,Int32 偏移量,Int32 计数, AsyncProtocolRequest asyncRequest)\r\n 在 System.Net.Security._SslStream.ProcessWrite(Byte[] 缓冲区, Int32 偏移量, Int32 计数, AsyncProtocolRequest asyncRequest)\r\n 在 System.Net.Security.SslStream.Write(Byte[ ] 缓冲区,Int32 偏移量,Int32 计数)\r\n

【问题讨论】:

  • 我能看到的唯一可能导致对单个文件的写入变得和/或出现缓慢的情况是,您没有在每次调用 fileStream.Write(buffer, 0, bytesRead); 后刷新文件
  • 不要在每个线程中打开同一个文件。打开一次并使用单个实例(确保多个线程不会同时写入 - 您可以使用 lock
  • 这应该可以。发布异常 ToString。网络有多快,你的超时有多大? (注意,Parallel.For 不适合,因为它使用了无法控制的并行度。您只能指定最大值。)
  • @usr 我从中获取文件的服务器是引发异常的原因。它指出“SocketException:连接尝试失败,因为连接方在一段时间后没有正确响应,或者由于连接的主机未能响应而建立的连接失败”。我认为这意味着接收器没有足够快地从流中读取字节。
  • @shortspider 这通常意味着您尝试连接到不存在的机器。 (如果它存在,你会在很短的时间内得到一个成功的连接或一种 connection denied 消息)

标签: c# multithreading file io parallel.for


【解决方案1】:

除非您正在写入条带 RAID,否则您不太可能通过同时从多个线程写入文件来体验性能优势。事实上,更有可能是相反的——并发写入会交错并导致随机访问,从而导致磁盘寻道延迟,这使得它们比大型顺序写入慢几个数量级。

要获得透视感,请查看latency comparisons。从磁盘连续读取 1 MB 需要 20 毫秒;写入大约需要相同的时间。另一方面,每次磁盘寻道大约需要 10 毫秒。如果您的写入以 4 KB 块交错,那么您的 1 MB 写入将需要额外 2560 毫秒的寻道时间,比顺序慢 100 倍。

我建议在任何时候只允许一个线程写入文件,并仅将并行性用于网络传输。您可以使用生产者-消费者模式,将下载的块写入有界并发集合(例如BlockingCollection&lt;T&gt;),然后由专用线程拾取并写入磁盘。

【讨论】:

  • 但是为什么会阻塞呢?
  • @usr:如果写入交错的粒度足够好,速度可能会下降几个数量级,使得写入看起来像是阻塞,而实际上非常慢。
  • @Douglas 所以我修改了我的代码以首先将文件创建为全零。然后当我运行所有线程时,我锁定文件写入。但是,我仍然遇到同样的错误。
  • 这个答案不考虑写入缓存(在文件系统或块设备层),或在磁盘上的本机命令队列。
  • @JonathonReinhart:有效的观察。我的计算提出了一个最坏情况分析,忽略了那些缓解优化。但是,我认为随机写入比大数据流慢几个数量级的观点仍然成立。磁盘缓冲区通常只有 32MB,因此很快就会被填满,在处理线程的缓冲数据流时仍会导致磁盘寻道。
【解决方案2】:
    fileStream.Seek(ranges[index].Item1, SeekOrigin.Begin);

Seek() 调用是个问题,您将寻找与当前文件结尾相距很远的文件部分。您的下一个 fileStream.Write() 调用会强制文件系统扩展磁盘上的文件,用零填充文件中未写入的部分。

这可能需要一段时间,您的线程将被阻塞,直到文件系统完成扩展文件。可能足够长以触发超时。您会在转移开始时看到这很早就出错了。

一种解决方法是创建在您开始写入真实数据之前填充整个文件。否则,下载者使用的一种非常常见的策略,您可能以前见过 .part 文件。另一个不错的好处是您可以很好地保证传输不会因为磁盘空间不足而失败。请注意,只有当机器有足够的 RAM 时,用零填充文件才便宜。 1 GB 在现代机器上应该不是问题。

复制代码:

using System;
using System.IO;
using System.Diagnostics;

class Program {
    static void Main(string[] args) {
        string path = @"c:\temp\test.bin";
        var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Write);
        fs.Seek(1024L * 1024 * 1024, SeekOrigin.Begin);
        var buf = new byte[4096];
        var sw = Stopwatch.StartNew();
        fs.Write(buf, 0, buf.Length);
        sw.Stop();
        Console.WriteLine("Writing 4096 bytes took {0} milliseconds", sw.ElapsedMilliseconds);
        Console.ReadKey();
        fs.Close();
        File.Delete(path);
    }
}

输出:

Writing 4096 bytes took 1491 milliseconds

那是在快速 SSD 上,主轴驱动器将花费更多更长的时间。

【讨论】:

  • 试过了,不幸的是没用。我在 Parallel.For 之外创建了文件并一直寻找到最后。仍然有同样的错误。
  • 不要只是寻找,那只会重现原来的问题。您必须实际调用 FileStream.Write()。你写什么并不重要。
  • 不,这也不起作用,同样的问题。 NTFS 文件系统处理“稀疏”文件的能力太好了,文件的大小不必与磁盘上的实际字节数相匹配。
  • @HansPassant 抱歉,我先调用了 seek,然后是 WriteByte,可以吗?
【解决方案3】:

这是我从目前提供的信息中的猜测:

在 Windows 上,当您写入扩展文件大小的位置时,Windows 需要将其之前的所有内容初始化为零。这可以防止旧磁盘数据泄漏,这将是一个安全问题。

很可能,除了您的第一个线程之外,所有线程都需要将如此多的数据归零,以至于下载超时。这不再是真正的流式传输,因为第一次写入需要很长时间。

如果您拥有 LPIM 权限,则可以避免零初始化。否则,出于安全原因,您不能。免费下载管理器会显示一条消息,它会在每次下载开始时进行零初始化。

【讨论】:

  • 试过了,没用。我遵循了@Hans Passant 的建议,虽然创建了一个归零文件,但多线程写入仍然失败。
【解决方案4】:

所以在尝试了所有建议后,我最终使用了MemoryMappedFile 并打开了一个流以写入每个线程上的MemoryMappedFile

int numberOfStreams = 4;
List<Tuple<int, int>> ranges = new List<Tuple<int, int>>();
string fileName = @"C:\MyCoolFile.txt";
//Ranges list populated here
using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(fileName, FileMode.OpenOrCreate, null, fileSize.Value, MemoryMappedFileAccess.ReadWrite))
{
    Parallel.For(0, numberOfStreams, index =>
    {
        try
        {
            HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create("Some URL");
            using(Stream responseStream = webRequest.GetResponse().GetResponseStream())
            {
                using (MemoryMappedViewStream fileStream = mmf.CreateViewStream(ranges[index].Item1, ranges[index].Item2 - ranges[index].Item1 + 1, MemoryMappedFileAccess.Write))
                {
                    responseStream.CopyTo(fileStream);
                }
            };
        }
        catch (Exception e)
        {
            exception = e;
        }
    });
}

【讨论】:

    【解决方案5】:

    System.Net.Sockets.NetworkStream.Write

    堆栈跟踪显示错误发生在写入服务器时。这是一个超时。这可能是因为

    1. 网络故障/过载
    2. 服务器无响应。

    这不是写入文件的问题。分析网络和服务器。可能服务器还没有准备好并发使用。

    通过禁用写入文件来证明这一理论。错误应该仍然存在。

    【讨论】:

    • 3.不存在的服务器,例如 telnet 1.2.3.4。由于使用现有服务器,您很可能不会遇到超时异常。即时连接被拒绝是更符合预期的响应。
    • @EZI 堆栈显示(哈哈!)这是连接已经发生后的写入(SslStream.Write)。连接就在那里。
    • usr, :) 这次你可能是对的。我不太确定。
    • @usr 所以我在两台服务器中都有 RDPd 并且已经将 Visual Studio 连接到这两个进程。当我开始下载时,我可以看到从目标服务器到源服务器的两个请求。我可以看到消息来源的回应。几秒钟后,源服务器上的一个连接(我正在使用两个线程)将引发该异常。另一个继续就好了。在目标服务器上,我只剩下一半的文件 + 一些额外的文件。我不认为这是网络问题。
    • @shortspider OK 创建一个测试用例,就像我的第一条评论一样。只需在搜索+写入时锁定文件流。
    猜你喜欢
    • 2013-12-02
    • 1970-01-01
    • 2019-02-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多