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