Ruby におけるハッシュのスレッドセーフ
概要
Ruby のハッシュのスレッド セーフについて興味があります。コンソールから次のコマンドを実行します (Ruby 2.0.0-p247)。
h = {}
10.times { Thread.start { 100000.times {h[0] ||= 0; h[0] += 1;} } }
戻り値
{0=>1000000}
これは正しい期待値です。
なぜ効果があるのでしょうか?このバージョンの Ruby ではハッシュがスレッドセーフであると信頼できますか?
編集: 100 回テスト:
counter = 0
100.times do
h={}
threads = Array.new(10) { Thread.new { 10000.times { h[0] ||= 0; h[0] += 1 } } }
threads.map { |thread| thread.join }
counter += 1 if h[0] != 100000
end
puts counter
最後までカウンターは0のままです。最大 10,000 回試しましたが、このコードではスレッド セーフティの問題が 1 つも発生しませんでした。
解決策
いいえ、ハッシュがスレッド セーフであることに依存することはできません。おそらくパフォーマンス上の理由から、ハッシュはスレッド セーフになるように構築されていないからです。標準ライブラリのこれらの制限を克服するために、スレッド セーフ (コンカレント Ruby) または不変 (ハムスター) データ構造を提供する Gem が作成されました。これらにより、データへのアクセスがスレッドセーフになりますが、コードにはそれに加えて別の問題もあります。
出力は決定的ではありません。実際、私はあなたのコードを数回試したところ、結果として 544988 が得られました。コードでは、個別の読み取りステップと書き込みステップが関係している (つまり、アトミックではない) ため、古典的な競合状態が発生する可能性があります。 h[0] ||= 0 という式を考えてみましょう。これは基本的に h[0] || に変換されます。 h[0] = 0。 ここで、競合状態が発生するケースを簡単に構築できます。
データが破損しないことを確認したい場合は、ミューテックスを使用して操作をロックできます。
require 'thread'
semaphore = Mutex.new
h = {}
10.times do
Thread.start do
semaphore.synchronize do
100000.times {h[0] ||= 0; h[0] += 1;}
end
end
end
注: この回答の以前のバージョンでは、「thread_safe」gem について言及していました。 「thread_safe」は 2017 年 2 月以降非推奨となり、「concurrent-ruby」gem の一部になりました。代わりにそれを使用してください。