QA@IT

RubyのFile.openは明示的にcloseしないと何が起こる?

17730 PV

RubyでWebページから読み込んだHTMLのテキストデータをローカルにキャッシュするために以下のように書きました。

    if File.exists? cache_file
      html = File.open(cache_file).read
    else
      html = open(url).read.toutf8.gsub(/\r/, "\n")
      File.open(cache_file, "w").write html
    end
  html
end

File.open(file).read はイディオムのように覚えていて、ファイルを開いて読みきって閉じてくれる(f.close)と理解しています。一方、"w"オプションで書き込み用に開いたファイルは明示的に閉じないといけないのでは? という気がしてきて、混乱しました。

File.open(cache_file, "w") {|f| f.write html; f.close}

のように書かないと、ファイルディスクリプタがリークしたりしないのでしょうか。と書いてから、File.open(file).read も同様に f.close 的なことをしないといけなかったのではと不安になりました。

もし、リークするのだとして、副作用として実際何が起こるのでしょうか? バッチ的に走らせるスクリプトなので、どっちみち処理が終わればOSがファイルディスクリプタの面倒も見てくれるという理解なのですが。

回答

いくつかの質問が混在していますが、ご心配しているとおり明示的なcloseが必要です。

File.open(file).read はイディオムのように覚えていて、ファイルを開いて読みきって閉じてくれる(f.close)と理解しています。

ということで、ここがまず誤解なさっている箇所です。File.openはブロックを取るとそのブロックを抜けたタイミングでcloseします。ですから、書き込みでは下記のように書けばきちんとcloseされます。

File.open(cache_file, "w") {|f| f.write html }

しかし、文中にあるようにブロックをとらずにFile.open(cache_file, "w")した場合はcloseがひつようです。

また、単に読み取るならクラスメソッドを使って

File.read(file)

と書けます。また、1.8.7以降ではSymbol#to_procを利用して、

File.open(file, &:read)

と書くこともできます。おそらくこれを見てイディオムとして覚えたのではないかと思いました。

リークするのだとして、副作用として実際何が起こるのでしょうか? バッチ的に走らせるスクリプトなので、どっちみち処理が終わればOSがファイルディスクリプタの面倒も見てくれるという理解なのですが。

はい。すぐに終了するプログラムで1つ2つあれば閉じなくてもあまり問題無いと思います。サーバプロセスなど長命だったり、ものすごくたくさん開いたままだとディスクリプタが足りなくなってファイルやソケットを作れなくなります。

編集 履歴 (0)
  • ブロックとリソース管理については http://yugui.jp/articles/408 の文章が力があって好きです。 -
  • ご指摘のとおり、File.readというクラスメソッドと、File#readというインスタンスメソッドを混同していました! -

File.open(cache_file, "w").write html
などとファイルデスクリプタはもちろんリークする…かにみえるのですが、
RubyにはGCという便利なものがあるので、FileオブジェクトがGCされたときにfdも閉じてくれます。
fdを使い切ってファイルを開くのに失敗した際にもGCが走るので、参照を残していなければ安心です。

fdを本当に使い切ると、fdを新規に作るものが全て失敗するので、
ファイルオープンやソケット作成に失敗するんですが、
忘れがちなものだと require や新規エンコーディングの読み込み等も失敗します。

なお、プロセスが確保したfdはOSが記録しており、プロセス終了時に全て解放されます。
ちなみに、fdの個数制限は ulimit -n で見れます。
手元のFreeBSDでは11095、CentOS/Ubuntuでは1024、NetBSDでは128となっていました。

編集 履歴 (0)
  • ありがとうございます! 参照を残さなければGCでオブジェクトごとリソースが開放されるのですね、なるほど。Mac OS X v10.7.3(Lion)で、ulimit -n は256でした。意外に少ないんですね。 -
ウォッチ

この質問への回答やコメントをメールでお知らせします。