在 Ruby 中,关闭 StringIO 是必要的吗?

18

在Ruby中,我们是否需要在使用完StringIO对象后关闭它们以释放资源,就像我们使用真正的IO对象一样?

obj = StringIO.new "some string"
#...
obj.close # <--- Do we need to close it?

完善我的问题

关闭文件对象是必要的,因为它会关闭文件描述符。在操作系统中打开的文件数量是有限制的,这就是为什么需要关闭文件的原因。但是,如果我理解正确,StringIO是一个存在于内存中的抽象概念。我们需要关闭它吗?

5个回答

22
  • StringIO#close方法不会释放任何资源或删除其对累加字符串的引用。因此调用它不会影响资源使用。

  • 只有在垃圾回收期间调用的StringIO#finalize方法才会释放对累加字符串的引用,以便可以释放它(前提是调用者没有保留自己的引用)。

  • 创建StringIO实例的方法StringIO.open在返回后不会保留对该实例的引用;因此,该实例对累加字符串的引用可以被释放(前提是调用者没有保留自己的引用)。

  • 实际上,在使用StringIO时很少需要担心内存泄漏。只要在完成使用后不再保留对StringIO的引用,一切都会很好。


深入源代码

StringIO实例所使用的唯一资源就是它正在累加的字符串。你可以在stringio.c (MRI 1.9.3)中看到这一点;这里显示了一个保存StringIO状态的结构体:

static struct StringIO *struct StringIO {
    VALUE string;
    long pos;
    long lineno;
    int flags;
    int count;
};

当StringIO实例被最终化(也就是被垃圾回收),它对字符串的引用会被丢弃,这样如果没有其他引用存在,字符串就可以被垃圾回收了。这是finalize方法,也被StringIO#open(&block)调用以关闭该实例。

static VALUE
strio_finalize(VALUE self)
{
    struct StringIO *ptr = StringIO(self);
    ptr->string = Qnil;
    ptr->flags &= ~FMODE_READWRITE;
    return self;
}

当对象被垃圾回收时,仅会调用finalize方法。 StringIO没有其他可以释放字符串引用的方法。

StringIO#close只是设置一个标记。它不会释放对累积字符串的引用或以任何其他方式影响资源使用:

static VALUE
strio_close(VALUE self)
{   
    struct StringIO *ptr = StringIO(self);
    if (CLOSED(ptr)) {
        rb_raise(rb_eIOError, "closed stream");
    }
    ptr->flags &= ~FMODE_READWRITE;
    return Qnil;
}

最后,当你调用StringIO#string时,你会得到一个指向和StringIO实例累积的完全相同的字符串的引用:

static VALUE
strio_get_string(VALUE self)
{   
    return StringIO(self)->string;
}

如何在使用StringIO时泄漏内存

所有这些意味着,只有一种方法可以导致StringIO实例引起资源泄漏:您不能关闭StringIO对象,并且您必须保留它比调用StringIO#string时获得的字符串更长时间。例如,想象一个类具有StringIO对象作为实例变量:

class Leaker

  def initialize
    @sio = StringIO.new
    @sio.puts "Here's a large file:"
    @sio.puts
    @sio.write File.read('/path/to/a/very/big/file')
  end

  def result
    @sio.string
  end

end

假设该类的用户获取结果后,简要使用并丢弃它,但仍保留对Leaker实例的引用。您可以看到Leaker实例通过未关闭的StringIO实例保留对结果的引用。如果文件非常大或者存在许多Leaker实例,则可能会出现问题。这个简单(而有意)的例子可以通过不将StringIO作为实例变量保留来解决。当可以时(通常都可以),与其费心明确地关闭它,还不如直接丢弃StringIO对象:

class NotALeaker

  attr_reader :result

  def initialize
    sio = StringIO.new
    sio.puts "Here's a large file:"
    sio.puts
    sio.write File.read('/path/to/a/very/big/file')
    @result = sio.string
  end

end

还有一点要补充的是,只有在字符串很大或StringIO实例很多并且StringIO实例的生命周期很长时,这些泄漏才会有影响。因此,可以看出明确关闭StringIO实例是很少甚至从未需要的。


然而,任何强制垃圾收集器运行的事情都是不好的。在高负载下,垃圾收集器是一个很大的性能问题。 - Linuxios
据我所知,“当StringIO实例被关闭时,字符串引用会被删除”是错误的。s = StringIO.new; s << "foobar"; s.close ; s.string将会得到"foobar" - Andrew Grimm
@Andrew,你好,很高兴见到你。我的意思是当StringIO实例关闭时,它对字符串的引用会被删除。你可以在终结器的代码中看到这一点(终结器既在实例关闭时调用,也在GC时调用)。感谢你指出这一点 - 我会看看能否让它更清晰明了。 - Wayne Conrad
@Andrew,最新消息:#close不会释放任何引用。只有StringIO.open隐式关闭才会释放引用。需要进行更多编辑... - Wayne Conrad

4

作为其他答案的回应:调用 close 并不能帮助您节省内存。

require "stringio"
sio = StringIO.new
sio.print("A really long string")
sio.close
sio.string # => "A really long string"

"

一个非常长的字符串

"将与sio一起存在,无论是否close
那么为什么StringIO有一个close方法呢?鸭子类型。提供close,并在尝试读取或写入时抛出IOError,确保其像真正的File对象一样运行。如果您在单元测试期间将其用作模拟对象,则这很有用。

3
关闭一个File非常重要,因为系统有限制的描述符数量(假设你在UNIX上,我不知道Windows会怎么样)。对于StringIO,还有另一种资源需要考虑:内存。如果StringIO对象需要存储大量数据,将会占用大量堆内存。另一方面,垃圾回收器总是关闭它所占用的IO对象,但这假定你拥有一种优雅的方法使你的对象超出作用域。此外,在高负载系统中,物理RAM比文件描述符更有价值。在我的Linux计算机上,最大文件描述符数为178203。我认为你无法达到那个数。

StringIO对象仍然会保存您写入其中的任何内容,直到它被垃圾回收。据我所知,调用closeclose_write不会改变任何东西。 - Andrew Grimm
嗯,我猜这就否定了我的观点。尽管如@WayneConrad在下面的回答中所说,调用finalize可能仍然是个好主意。不过,有时候使用StringIO.open会使代码更清晰,特别是你在下面建议的情况下。 - Linuxios

0
一般来说,答案是否定的。当垃圾回收器占用I/O流时,它们会自动关闭。对于文件I/O也是同样的答案。

1
“当被垃圾回收器占用时会自动关闭”不是你想要的,因为你无法预测何时会发生这种情况。这就是为什么你在处理文件时要使用 File.open { |f| ... },并且你也应该在使用 StringIO 时采用同样的做法。 - Niklas B.
2
是的,但通常情况下(特别是对于真实文件),答案是肯定的,而不是否定的。 - Niklas B.
1
等等,等等。您能告诉我为什么需要关闭StringIO而不是File吗?据我所知,关闭文件主要是为了释放文件描述符,而不是为了释放内存。如果我的理解是正确的,那么为什么我们需要关闭StringIO呢? - Evgenii
@Evgeny:File.close 还会释放内存缓冲区,因此它也会释放内存。在使用 StringIO 时,只有在释放内存。 - Niklas B.
1
关闭文件(当您与其无关时)对于文件的健康也是有好处的,如果系统崩溃发生。这就是为什么Ruby为您提供“原子”文件方法,它打开文件,对其进行操作,并自动关闭它。 - karatedog

-2

不是必须的,但它有助于内存优化。


在任何语言中,您都需要分配内存引用。在结束标记时,您通知任何语言您已经完成了该变量。并且垃圾收集器的处理方式在任何语言中都是相同的。 - ohm
2
StringIO在调用close_write后仍会将您写入其中的任何内容存储在其中。 - Andrew Grimm

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