方法中的“使用”语句会导致数据损坏或访问冲突的麻烦吗?

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 的回答。

以上是关于方法中的“使用”语句会导致数据损坏或访问冲突的麻烦吗?的主要内容,如果未能解决你的问题,请参考以下文章

无法访问J盘显示文件或目录损坏的资料找回方法

文件系统损坏导致虚拟机无法正常启动的问题及解决方法

移动硬盘无法访问文件或目录损坏且无法读取资料恢复的法子

深入解析Python中的线程同步方法

SQLSTATE [42000]:语法错误或访问冲突:sql select 语句中的 1064

如何修复损坏的MySQL数据表