どのような問題か
さいきんのrubyでは、Hashのiteration中にhashに破壊的な変更として新しいkeyを挿入しようとすると、can't add a new key into hash during iteration
というRuntimeErrorをあげてくれます。
コード:
# nyan.rb def iterate_hash_and_insert_key(hash) hash.each do |k, v| hash[:new_key] = :new_value end end hash = {a: :b} iterate_hash_and_insert_key(hash)
Traceback (most recent call last): 3: from nyan.rb:8:in `<main>' 2: from nyan.rb:2:in `iterate_hash_and_insert_key' 1: from nyan.rb:2:in `each' nyan.rb:3:in `block in iterate_hash_and_insert_key': can't add a new key into hash during iteration (RuntimeError)
スタックトレースをみれば、たしかにeachの中でhashにアクセスして新しいkeyを追加しようとしているのが見て取れるでしょう。
ここで、下記のようなコードとスタックトレースの場合を考えて見ましょう。
コード:
# nyan.rb def iterate_hash_loop(hash) hash.each do |k, v| loop do sleep 1 end end end def insert_key(hash) hash[:new_key] = :new_value end hash = {a: :b} t = Thread.new do iterate_hash_loop(hash) end sleep 1 # threadが始まるのを確実に待つ insert_key(hash) t.join
Traceback (most recent call last): 1: from nyan.rb:20:in `<main>' nyan.rb:10:in `insert_key': can't add a new key into hash during iteration (RuntimeError)
スタックトレースを見た限りでは、「いやいや、iterationの中でhashいじってないし」という感じがするのではないでしょうか。実際、 insert_key
はコードの上でもコールスタックの上でも、iterationの外で行われています。
ではなぜ can't add a new key into hash during iteration
RuntimeErrorが出ているのでしょうか。もちろん、別のスレッドで同時に(厳密には同時じゃないけどわかって)当のhashをiterationしているからです。
このように、スレッドセーフでないコードで複数の箇所からアクセスされるHashを触っている場合、「スタックトレースを見てもコードみても、iterationの外でhashにアクセスしてるはずなのになぜか can't add a new key into hash during iteration
RuntimeErrorが出るな〜」という一見不可解な状況に遭遇することがあります。一見なぞの状況で can't add a new key into hash during iteration
RuntimeErrorが出てしまう場合、別のThreadの動きを確認してみるといいかもしれません。
どのように解決すべきか
どのように解決すべきかと考えてみると、
- Mutexなどを利用して当該コードをスレッドセーフにする(正道な気がする)
- スレッドローカルな変数(
Thread#[]
とThread#[]=
)を利用してスレッドセーフにする(これも正道っぽい) - そもそもマルチスレッドやめる(やめれるならこれが一番シンプルになる)
あたりかなあと思っております。