can't add a new key into hash during iterationが謎の状況で発生しているように見えるときは

どのような問題か

さいきんの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#[]=)を利用してスレッドセーフにする(これも正道っぽい)
  • そもそもマルチスレッドやめる(やめれるならこれが一番シンプルになる)

あたりかなあと思っております。