我正在尝试使用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比写入多个文件时慢?所有磁盘操作都应该受磁盘速度的限制吗?任何人都可以解释这种行为吗?
提前致谢!
更新:以下是源服务器抛出的错误:
&#34;无法将数据写入传输连接:连接尝试失败,因为连接方在一段时间后没有正确响应,或者建立的连接失败,因为连接的主机无法响应。&#34; [System.IO.IOException]:&#34;无法将数据写入传输连接:连接尝试失败,因为连接方在一段时间后没有正确响应,或者建立的连接失败,因为连接的主机无法响应&#34; InnerException:&#34;连接尝试失败,因为连接方在一段时间后没有正确响应,或者建立的连接失败,因为连接的主机未能响应&#34; 消息:&#34;无法将数据写入传输连接:连接尝试失败,因为连接方在一段时间后没有正确响应,或者由于连接的主机无法响应而建立连接失败。&#34; StackTrace:&#34; System.Net.Sockets.NetworkStream.Write(Byte [] buffer,Int32 offset,Int32 size)\ 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
答案 0 :(得分:4)
除非您正在写入条带化RAID,否则您不太可能通过同时从多个线程写入文件来体验性能优势。实际上,它更可能是相反的 - 并发写入会交错并导致随机访问,导致磁盘搜索延迟,使得它们比大型顺序写入慢几个数量级。
要了解透视感,请查看一些latency comparisons。从磁盘读取的顺序1 MB需要20 ms;写作大约在同一时间。另一方面,每个磁盘寻道大约需要10毫秒。如果您的写入以4 KB块进行交错,则1 MB写入将需要额外的2560 ms寻道时间,使其比连续写入慢100倍。
我建议只允许一个线程随时写入该文件,并仅使用并行性进行网络传输。您可以使用生产者 - 消费者模式将下载的块写入有界并发集合(例如BlockingCollection<T>
),然后通过专用线程将其拾取并写入磁盘。
答案 1 :(得分:1)
以下是我对迄今为止所提供信息的猜测:
在Windows上,当您写入扩展文件大小的位置时,Windows需要将零初始化为它之前的所有内容。这可以防止旧磁盘数据泄漏,这可能是一个安全问题。
可能除了你的第一个线程之外的所有线程都需要零启动以下数据以便下载超时。这不是真正的流媒体,因为第一次写作需要很长时间。
如果您具有LPIM权限,则可以避免零初始化。否则你不能出于安全考虑。免费下载管理器显示一条消息,它在每次下载开始时都会启动零启动。
答案 2 :(得分:1)
fileStream.Seek(ranges[index].Item1, SeekOrigin.Begin);
Seek()调用是一个问题,您将寻找远离当前文件结尾的文件的一部分。你的下一个fileStream.Write()调用强制文件系统扩展磁盘上的文件,用零填充它的未写入部分。
这可能需要一段时间,您的线程将被阻止,直到文件系统完成扩展文件。可能足够长以触发超时。你很快就会在转会开始时看到这个问题。
解决方法是在开始编写实际数据之前创建和填充整个文件。除非是下载程序使用的一种非常常见的策略,否则您可能已经看过.part文件。另一个好处是你可以保证传输不会因为磁盘空间不足而失败。请注意,当机器有足够的RAM时,用零填充文件只是很便宜。 1 GB在现代机器上应该不是问题。
Repro代码:
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上,主轴驱动器需要更长时间
。答案 3 :(得分:0)
System.Net.Sockets.NetworkStream.Write
The stack trace shows that the errors happens when writing to the server. It is a timeout. This can happen because of
This is not an issue with writing to a file. Analyze the network and the server. Maybe the server is not ready for concurrent usage.
Prove this theory by disabling writing to the file. The error should remain.
答案 4 :(得分:0)
因此,在尝试了所有建议之后,我最终使用了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;
}
});
}