方法中的“使用”语句会导致数据损坏或访问冲突的麻烦吗?
Posted
技术标签:
【中文标题】方法中的“使用”语句会导致数据损坏或访问冲突的麻烦吗?【英文标题】:"using" statment inside method can cause troubles of data corruption or Access violation? 【发布时间】:2013-11-02 19:29:17 【问题描述】:我有一个任务,将数据设置到 FIFO 中,然后另一个线程将这些数据一个一个地读取到 FIFO 中,稍后通过网络发送。调用FIFO.Add
时转换为字节数组的数据如下:
public byte[] ByteArraySerialize()
using (MemoryStream m = new MemoryStream())
using (BinaryWriter writer = new BinaryWriter(m))
writer.Write((int)this.Opcode);
writer.Write(this.Data);
return m.ToArray();
我的问题:在发送者线程从 FIFO 中读取数据之前,数据是否可能已损坏或处置?我的问题是了解using
里面的方法:
这是在方法中使用using
的方式可能会导致GC 在线程读取数据之前删除MemoryStream
,比如说在数据进入FIFO 几秒或几分钟后?
【问题讨论】:
@Delnan:他没有返回 MemoryStream,而是返回一个数组复制自 MemoryStream。所以他的代码很好。 他还返回了 disposed 实例拥有的数据的副本。虽然这在MemoryStream
的当前实现中是可行的,但只要他不厌其烦地问这里是否有问题,那几乎不是正确的做事方式。我会说他的代码不没问题。
【参考方案1】:
阅读这个问题有多种方式,但让我们从显而易见的方式开始,即它的编写方式:
这种在方法中使用“using”的方式是否会导致 GC 在线程读取数据之前移除内存流,让我们说在数据进入 FIFO 后几秒或几分钟后?
没有。这不会是一个问题。如果您能够在调用.ToArray()
的过程中读取数据,那么您已经拥有数据的副本。如果 GC 稍后收集流,则该数组将继续存在。 明确,关于 GC,如果您可以在调用 .ToArray()
的位置读取流内部的良好副本,那么之后该数组就可以了。根据文档,您获得的是内部数据的副本,而不是对其的引用,即使如此,如果您引用了某些内部数据结构,GC 将无法收集它。
然而,另一种解释可能是:这段代码有问题吗?
嗯,是的,也不是。
BinaryWriter
的当前实现将在编写器实例被释放时释放底层流。这意味着MemoryStream
将被丢弃。
让我复制你的代码并添加一些 cmets:
public byte[] ByteArraySerialize()
using (MemoryStream m = new MemoryStream())
using (BinaryWriter writer = new BinaryWriter(m))
writer.Write((int)this.Opcode);
writer.Write(this.Data);
// m is really disposed here
return m.ToArray();
这有什么不同吗?嗯,不。在当前的实现中,处理内存流不会以任何方式丢弃它。但是对于当前的实现或其未来没有任何保证,这是未记录的行为。如果您希望此代码在 .NET 的未来版本或修补程序中稳定且值得信赖,我不会这样编写。
因此,我不会使用这种方式。我将代码重写如下:
using (MemoryStream m = new MemoryStream())
using (BinaryWriter writer = new BinaryWriter(m))
writer.Write((int)this.Opcode);
writer.Write(this.Data);
writer.Flush();
return m.ToArray();
这将要求编写器刷新所有数据,然后在释放该实例之前复制内存流的内部数组。
要么这样,要么使用重载的构造函数并要求编写者保持流打开:
using (MemoryStream m = new MemoryStream())
using (BinaryWriter writer = new BinaryWriter(m, Encoding.UTF8, true))
writer.Write((int)this.Opcode);
writer.Write(this.Data);
// m is no longer disposed here
return m.ToArray();
【讨论】:
好答案。只是一个几乎偏离主题的评论:原始问题和您的第一个示例中的编码隐含地像new UTF8Encoding()
。无参数构造函数不使用字节顺序标记。从静态属性Encoding.UTF8
获得的编码更像new UTF8Encoding(true)
,其中true
表示确实使用字节顺序标记。这有点令人困惑。但是在某些情况下根本不使用所谓的前导码,如果它是空的或由字节序标记组成,则无关紧要。
更加混乱。如果未指定任何内容,则使用的编码等于new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)
,而静态属性Encoding.UTF8
等于new UTF8Encoding(encoderShouldEmitUTF8Identifier: true, throwOnInvalidBytes: false)
。【参考方案2】:
对ToArray();
的调用有效地复制了您想要的数据。
因此,发生在 MemStreams 上的一切都是无关紧要的。
更一般地说,只要您的代码可以“看到”一段数据,那么 GC 就无法回收该段数据。 不要想太多。
假设你曾经使用过:
return m.GetBuffer();
现在您正在返回 MemStream 的内部缓冲区。 m
将被 Disposed 但仅仅因为您返回了它,缓冲区将比它的所有者更长寿。
【讨论】:
提问者不应将using
语句与 GC 行为混淆。除非Dispose
方法抑制了类具有的finalizer,否则using
和GC 之间没有关系。
我提到 GC 只是为了让我的问题变得尖锐,我的意思是处置对象及其所有资源。据我所知并想确定数据是否会被删除,这意味着我知道 C# 基本上将数据作为参考移动,所以问题是:数据是否会被复制,或者我可能会因为处置而出现“访问冲突”米?
我想我的回答是:不,它不会导致 AV。【参考方案3】:
我认为您的问题的答案是“在这种情况下不是”。 内存流当然可以被释放,但在此之前,您会将数据保存在 byte[] 数组中,该数组将保留。
尝试在writer.Write(this.Data);
之后添加writer.Flush();
。
【讨论】:
Flush() 会完成什么? (随后的关闭/处置不会?) 我在 .NET 中遇到过 Dispose 无法刷新的情况。 AFAIR 是 .NET 4.0 中的 XmlDictionaryWriter。而且, BinaryWriter.Dispose 的文档并没有说它会导致将任何缓冲数据写入底层设备。【参考方案4】:不,没有问题。流不会被过早释放。
您谈论的是 GC,但 using
语句和 IDisposable
的想法是,当对象超出范围时,任何资源都会立即释放。我们不必等待 GC。换句话说,这与 GC 无关。
【讨论】:
Stream 将在此方法返回之前被释放。没关系。 "too early"?,太早是什么意思,我需要确保数据不会被丢弃,只要它在FIFO中并且在发送之后。 @Joseph 好吧,流m
不会被处理因为最外层的using
语句 在字节数组被创建和写入之前,并且你只需要那个数组。但是,Lasse V. Karlsen 在他的回答(在我的回答之后)指出,BinaryWriter
writer
默认情况下会在 writer
被处置时处置其底层流,这发生在 之后最内层 using
语句完成,即在创建字节数组之前。但请参考 Karlsen 的回答。以上是关于方法中的“使用”语句会导致数据损坏或访问冲突的麻烦吗?的主要内容,如果未能解决你的问题,请参考以下文章