デバッグ中の発見
とあるマルチスレッドプログラムをPythonで書いていた際に遭遇した誤使用です。C/C++等の言語を使った場合には起こらない仕様なので、知っておくと役立つ時がくるはずです。これはPythonだけでなく、Rubyでも同様のことが起こるので、Rubyistさんも是非気をつけていただければと思います。
@ahaha_traderさんのご指摘により、C/C++でも同様のことが発生することを教えていただきました・・・。不勉強をお詫び申し上げます。
このブログを見ているような方々だとバグが生じる実際のコードと、結果をまずお見せしたほうが良いと思うので、サンプル用に作ったコードがこちらです。
バグが生じるソースコード
import Queue import threading class MultiThreadIncrement(object): def __init__(self, thread_amount): self.thread_amount = thread_amount self.thread_array = [None] * thread_amount self.processing_count = 0 def make_threads(self): for i in xrange(self.thread_amount): start_queue = Queue.Queue() result_queue = Queue.Queue() close_event = threading.Event() self.thread_array[i] = threading.Thread(target=self.thread_target, args=(start_queue, result_queue, close_event)) self.thread_array[i].start() self.thread_array[i].start_queue = start_queue self.thread_array[i].result_queue = result_queue self.thread_array[i].close_event = close_event def increment(self): for i in xrange(self.thread_amount): self.thread_array[i].start_queue.put(None) for i in xrange(self.thread_amount): self.thread_array[i].result_queue.get() def close(self): for i in xrange(self.thread_amount): self.thread_array[i].close_event.set() self.thread_array[i].start_queue.put(None) def thread_target(self, start_queue, result_queue, close_event): while True: start_queue.get() if close_event.is_set(): return self.processing_count += 1 result_queue.put(True) if __name__ == "__main__": mult = MultiThreadIncrement(12) mult.make_threads() for _ in xrange(1000): mult.increment() print mult.processing_count mult.close()
やっている内容は、12個のスレッドを使って self.increment_count + 1 を12,000回繰り返すという単純処理です。
実行結果
increment result : 11971
本来、12*1,000の12,000を表示させたかったはずのこのコードなのですが、残念ながら29の違いが生じています。なぜこれが生じてしまうのでしょうか。
動作不良の理由
C/C++など、正式なインクリメントが適応されているような言語ですと、マルチスレッドで行っても、確実にプロセッサレベルで正確にインクリメント処理が行われるので問題がありません。
ですが、PythonやRubyのインクリメント処理は、C/C++で使用されるようなインクリメント処理とはまったく異なります。PythonやRubyの場合のインクリメントの記述自体は、インクリメントっぽく書かれていますが、実際に行われているのは以下の処理です。
# increment_count += 1 increment_count = increment_count + 1
ということは、increment_count + 1 の演算が行われた後の結果のPython/Rubyオブジェクトにincrement_countの参照が適応されます。簡単に言うと、演算と結果の格納が2つの処理になってしまっているのです。
この場合、マルチスレッドにて、increment_count + 1 の演算がポインタの割り当てより前に他のスレッドにて同時に行われてしまうというケースが生じます。それが、今回の検証の場合 29/12,000 の割合で生じてしまっていたのですね。
参考URL
誤使用の回避方法
回避する方法は、Queue.Queueクラスを使い、putされた個数を取得するという方法が簡単で便利です(12,000回もカウントするのには実用的ではないけれど)。他にもthreading.Lockを使う方法もありますが、マルチスレッドの利点が減ってしまうかもですね。
なかなかデバッグしにくい内容で手こずりましたが、バグの原因が特定できると納得の行く結果となりました。みなさんもお気をつけ下さいね!
コメント