在Ruby中,我们是否需要在使用完StringIO对象后关闭它们以释放资源,就像我们使用真正的IO对象一样?
obj = StringIO.new "some string"
#...
obj.close # <--- Do we need to close it?
完善我的问题
关闭文件对象是必要的,因为它会关闭文件描述符。在操作系统中打开的文件数量是有限制的,这就是为什么需要关闭文件的原因。但是,如果我理解正确,StringIO是一个存在于内存中的抽象概念。我们需要关闭它吗?
在Ruby中,我们是否需要在使用完StringIO对象后关闭它们以释放资源,就像我们使用真正的IO对象一样?
obj = StringIO.new "some string"
#...
obj.close # <--- Do we need to close it?
完善我的问题
关闭文件对象是必要的,因为它会关闭文件描述符。在操作系统中打开的文件数量是有限制的,这就是为什么需要关闭文件的原因。但是,如果我理解正确,StringIO是一个存在于内存中的抽象概念。我们需要关闭它吗?
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实例是很少甚至从未需要的。
作为其他答案的回应:调用 close
并不能帮助您节省内存。
require "stringio"
sio = StringIO.new
sio.print("A really long string")
sio.close
sio.string # => "A really long string"
一个非常长的字符串
"将与sio
一起存在,无论是否close
。close
方法呢?鸭子类型。提供close
,并在尝试读取或写入时抛出IOError,确保其像真正的File
对象一样运行。如果您在单元测试期间将其用作模拟对象,则这很有用。File
非常重要,因为系统有限制的描述符数量(假设你在UNIX上,我不知道Windows会怎么样)。对于StringIO
,还有另一种资源需要考虑:内存。如果StringIO
对象需要存储大量数据,将会占用大量堆内存。另一方面,垃圾回收器总是关闭它所占用的IO
对象,但这假定你拥有一种优雅的方法使你的对象超出作用域。此外,在高负载系统中,物理RAM比文件描述符更有价值。在我的Linux计算机上,最大文件描述符数为178203。我认为你无法达到那个数。close
或close_write
不会改变任何东西。 - Andrew Grimmfinalize
可能仍然是个好主意。不过,有时候使用StringIO.open
会使代码更清晰,特别是你在下面建议的情况下。 - LinuxiosFile.open { |f| ... }
,并且你也应该在使用 StringIO
时采用同样的做法。 - Niklas B.File.close
还会释放内存缓冲区,因此它也会释放内存。在使用 StringIO
时,只有在释放内存。 - Niklas B.不是必须的,但它有助于内存优化。
close_write
后仍会将您写入其中的任何内容存储在其中。 - Andrew Grimm
s = StringIO.new; s << "foobar"; s.close ; s.string
将会得到"foobar"
。 - Andrew Grimm