内存流和大对象堆

16
我需要使用WCF在不可靠的连接之间传输大文件。为了能够恢复文件并且不希望WCF限制我的文件大小,我将文件分成1MB的块,并且这些“块”被作为流进行传输。目前看来效果不错。
我的步骤如下:
1. 打开文件流 2. 从文件中读取块到byte[]并创建MemoryStream 3. 传输块 4. 回到第2步,直到整个文件被发送
我的问题在于第2步。我认为当我从一个字节数组创建一个内存流时,它会最终出现在LOH上,导致OutOfMemory异常。但是实际上我没有遇到过这个错误,所以我可能对我的假设不正确。
现在,我不想在消息中发送byte[],因为WCF会告诉我数组大小太大。我可以更改允许的最大数组大小和/或我的块大小,但是我希望有其他解决方案。
我的实际问题是:
1. 我当前的解决方案是否会在LOH上创建对象,是否会引起问题? 2. 有更好的解决方法吗?
另外,在接收端,我只需从到达的流中读取较小的块并将它们直接写入文件中,因此没有涉及大型字节数组。
编辑: 当前解决方案:
for (int i = resumeChunk; i < chunks; i++)
{
 byte[] buffer = new byte[chunkSize];
 fileStream.Position = i * chunkSize;
 int actualLength = fileStream.Read(buffer, 0, (int)chunkSize);
 Array.Resize(ref buffer, actualLength);
 using (MemoryStream stream = new MemoryStream(buffer)) 
 {
  UploadFile(stream);
 }
}
4个回答

40

我希望这样做没问题。这是我在StackOverflow上的第一个回答。

是的,如果你的chunksize超过85000字节,那么数组将被分配到大对象堆中。你可能不会很快耗尽内存,因为你正在分配和释放连续的、相同大小的内存区域,所以当内存填满时,运行时可以将一个新的块适配到旧的、已回收的内存区域中。

我有点担心Array.Resize调用,因为它会创建另一个数组(见http://msdn.microsoft.com/en-us/library/1ffy6686(VS.80).aspx)。如果actualLength==Chunksize(对于除了最后一个块之外的所有块都是这样的),这是一个不必要的步骤。所以我至少建议:

if (actualLength != chunkSize) Array.Resize(ref buffer, actualLength);

这样可以减少很多内存分配。如果实际大小与块大小不同但仍大于85000,则新数组也可能在大对象堆上分配,导致它碎片化并可能导致表面上的内存泄漏。我相信即使泄漏速度缓慢,最终耗尽内存仍然需要很长时间。

我认为更好的实现方式是使用某种缓冲池提供数组。您可以自行构建(过于复杂),但WCF确实为您提供了一个。我稍微修改了您的代码以利用它:

BufferManager bm = BufferManager.CreateBufferManager(chunkSize * 10, chunkSize);

for (int i = resumeChunk; i < chunks; i++)
{
    byte[] buffer = bm.TakeBuffer(chunkSize);
    try
    {
        fileStream.Position = i * chunkSize;
        int actualLength = fileStream.Read(buffer, 0, (int)chunkSize);
        if (actualLength == 0) break;
        //Array.Resize(ref buffer, actualLength);
        using (MemoryStream stream = new MemoryStream(buffer))
        {
            UploadFile(stream, actualLength);
        }
    }
    finally
    {
        bm.ReturnBuffer(buffer);
    }
}

假设 UploadFile 的实现可以被重写,以便使用一个整数来表示要写入的字节数。

希望这可以帮到你。

joe


1
非常感谢!这真的是一个很棒的答案。感谢您指出Array.Resize问题。我也从未听说过BufferManager,听起来这将在其他领域对我有所帮助。这比我预期的要多得多,所以我考虑开始一个小赏金并把它给你,但我必须等待23小时才能开始一个赏金...所以你也必须等待 :) - flayn
谢谢。我很高兴能够帮忙。如果还有其他需要,请告诉我。回过头来看,值得指出的是,最佳实现应该在整个服务中共享单个BufferManager实例。我不知道这对你来说是否实际。 - Joe Simmonds
刚刚在寻找类似问题的答案时偶然发现了这个。以前从未听说过BufferManager - 太棒了!我想这将是未来需要记住的东西。 - alexander.biskop
我认为你也可以这样做:var stream = new MemoryStream(buffer, 0, actualLength) - SergioL
1
我认为 "if (actualLength != chunkSize)" 是不必要的...根据 Array.Resize 文档 "如果 newSize 等于旧数组的长度,则此方法不执行任何操作。" - Cameron Peters

9

另请参阅可回收内存流。 来自本文

Microsoft.IO.RecyclableMemoryStream是一个MemoryStream替代品,为性能关键的系统提供了更优秀的行为。特别是它被优化用于执行以下操作:

  • 通过使用池化缓冲区来消除大对象堆分配
  • 减少gen 2 GCs的数量,并因GC而暂停的时间要少得多
  • 通过具有有限池大小来避免内存泄漏
  • 避免内存碎片
  • 提供出色的调试性
  • 提供性能跟踪指标

2

我对你问题的前半部分不太确定,但是关于更好的方式——你考虑过BITS吗?它允许在后台通过http下载文件。你可以提供http://或file:// URI。它可以从被中断的点继续下载,并且使用http HEADER中的RANGE方法以字节块的形式下载。Windows Update也使用它。你可以订阅事件以获取有关进度和完成情况的信息。


谢谢您的建议,但我不想在每台机器上安装IIS。 - flayn
2
没问题,只是一个想法。只需在每台上传机器上安装IIS即可。如果客户端仅使用BITS进行下载,则不需要IIS。 - Peter Kelly

1

我想到了另一种解决方案,你觉得怎么样?

由于我不想在内存中存储大量数据,所以我正在寻找一种优雅的方法来临时存储字节数组或流。

我的想法是创建一个临时文件(您不需要特定的权限来执行此操作),然后将其用作内存流。将该类设置为可处理的(Disposable)将在使用后清理临时文件。

public class TempFileStream : Stream
{
  private readonly string _filename;
  private readonly FileStream _fileStream;

  public TempFileStream()
  {
     this._filename = Path.GetTempFileName();
     this._fileStream = File.Open(this._filename, FileMode.OpenOrCreate, FileAccess.ReadWrite);
  }

  public override bool CanRead
  {
   get
    {
    return this._fileStream.CanRead;
    }
   }

// and so on with wrapping the stream to the underlying filestream

...

    // finally overrride the Dispose Method and remove the temp file     
protected override void Dispose(bool disposing)
  {
      base.Dispose(disposing);

  if (disposing)
  {
   this._fileStream.Close();
   this._fileStream.Dispose();

   try
   {
      File.Delete(this._filename);
   }
   catch (Exception)
   {
     // if something goes wrong while deleting the temp file we can ignore it.
   }
  }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接