Techioz Blog

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 の一部になりました。代わりにそれを使用してください。