RubyMotionでシングルトン

Singletonモジュールは使えないんですよ。ええ。

Dispatch.once

そこでDispatch.onceを使うといいらしい。

参考: RubyMotion gets iOS 6, iPhone 5, debugger – RubyMotion Blog

以下のように書く。

class Hoge
  def self.instance
    Dispatch.once { @@instance ||= new }
    @@instance
  end
end

Dispatch.onceはブロックとして渡したコードがアプリの起動中に一度しか実行されないことを保証してくれる。

複数のスレッドからほぼ同時にアクセスされた場合、コンマ一秒でも速くDispatch.onceまでたどり着いた方のスレッドで実行され、遅れたスレッドではDispatch.onceに渡されたブロックが終了するまで待機するので、確実に単一のインスタンスが得られる。

Dispatch.onceを使わない以下のコードでも一見うまく動くが、初期化処理が重かったりすると複数のスレッドからnewがそれぞれ実行され、別々のインスタンスが返る可能性がある。

class Hoge
  def self.instance
    @@instance ||= new
  end
end

本当に期待通りにちゃんと動くのか?

確かめた。
以下のコードをapp/app_delegate.rbにベタっと書いてrake。

class AppDelegate
  def application(application, didFinishLaunchingWithOptions:launchOptions)
    true

    @async_done1 = false
    @async_done2 = false
    queue_group = Dispatch::Group.new
    q1 = Dispatch::Queue.concurrent(:default)
    q2 = Dispatch::Queue.concurrent(:default)

    q1.async(queue_group) { i1 = DispatchOnceSample.instance(1); p "#{1} #{i1.class} object_id: #{i1.object_id}[#{i1.surely_singleton}]" }
    q2.async(queue_group) { i2 = DispatchOnceSample.instance(2); p "#{2} #{i2.class} object_id: #{i2.object_id}[#{i2.surely_singleton}]" }
    queue_group.notify(q1) { @async_done1 = true }
    queue_group.notify(q2) { @async_done2 = true }

    CFRunLoopRunInMode(KCFRunLoopDefaultMode, 0.1, false) while !(@async_done1 && @async_done2)
  end
end

class DispatchOnceSample
  attr_accessor :surely_singleton

  def self.instance(id)
    Dispatch.once{ @@instance ||= new(id) }
    # @@instance ||= new(id)
    @@instance
  end

  def initialize(id)
    @surely_singleton = false
    Dispatch.once{ @surely_singleton = true }
    p "initialize starting: #{id}"
    if id == 1
      p "waiting..."
      3.times {|i| p i; sleep 1 }
    end
    p "initialize finished: #{id}"
  end
end

重いときちゃんと待ってくれるのだろうかと。
出力は以下のようになる。

"initialize starting: 1"
"waiting..."
0
(main)> 1
2
"initialize finished: 1"
"1 DispatchOnceSample object_id: 123990272[true]"
"2 DispatchOnceSample object_id: 123990272[true]"

もしくは以下。

"initialize starting: 2"
"initialize finished: 2"
"1 DispatchOnceSample object_id: 278392144[true]""2 DispatchOnce object_id: 278392144[true]"

q1とq2のどちらが速くDispatch.onceにたどり着くかは実行のたびにまちまちだが、しっかりと単一のインスタンスがどちらのスレッドでも得られる。
またq1が実行権を取った場合は時間のかかる処理となるが、q2は実行終了まで待機してからインスタンスを返しているのが分かる。

newを禁止してないので厳密なシングルトンにしたければもう少しがんばる必要があるが、とりあえずこれくらいの緩いシングルトンでも実用上は問題ないだろう。

Dispatch.onceを使わないと?

以下のような結果になる。

"initialize starting: 2""initialize starting: 1"

"waiting..."
"initialize finished: 2"
0
"2 DispatchOnceSample object_id: 124050304[false]"
(main)> 1
(main)> 2
"initialize finished: 1"
"1 DispatchOnceSample object_id: 120979280[true]"

object_idが異なる別々のインスタンスが生成されてしまっており、シングルトンにしたつもりがシングルトンになっていない。
場合によっては再現性が不確かなやっかいなバグが潜り込む可能性がある。