マルチスレッドで実行する 同期編

 初期編ではフォアグラウンドで処理をする裏でバックグラウンドで別の処理をするということを説明しました。これは二つの処理を一遍にやっていますが、お互いの処理が完全に別々でした。
 今回は、まず複数の処理で1つのオブジェクトや変数を取り合うという状態、競合状態という問題について説明し、この取り合いを解決するための同期という仕組みについて説明したいと思います。

 競合状態を発生させる

 まず百聞は一見にしかず。まず競合を起こすスクリプトを書いてみましょう。

import System
from System.Threading import *

class MyClass:
  def __init__(self):
    self.count = 0

  #マルチスレッドで行う処理
  def Increment(self):
    if self.count <10 :
      Thread.Sleep(100) ←競合状態を起こしやすくするトラップです。
      self.count = self.count +1
      print self.count

  #100のスレッドを一気に実行します。
  def start(self):
    for i in xrange(100):
    th = Thread(ThreadStart(self.Increment))
    th.Start()

a = MyClass()

a.start()

raw_input()

 後で同期システムを説明しやすいようにクラスを使用しました。ではマルチスレッドで行う処理を見ていきましょう。

 Thread.Sleep(100)

 これはThreadクラスのSleepメゾットです。100ミリ秒処理を待機するように記述しています。これは競合状態にさせるためのトラップで意味はありません。

 if self.count <10 :
   self.count = self.count +1
   print self.count

 それ以外のスクリプトはcountが10未満であれば、countに1を足して表示するといった単純なコードです。なので、これだけ見れば、10以下の数字が表示されるはずが、それを上記のように100のスレッドを一気に実行したらどうでしょう。 10よりも大きな数字がバンバン表示されませんか。

 理由は次の通りです。例えば一つ目のスレッドがcountを確認して10未満なので、If文以下のブロックを実行にしたとします。しかし、Thread.Sleep(100)があるために、スゴロクでいうところの一旦休み。いざcountをprintしようとしてみると、別のスレッドにcountを書き換えられて、先程評価したcountとは違う値が表示されているんです。
 まるでTVのチャンネル争い、自分の好きな番組にしたのに、トイレに行っているうちに別の番組に替えられちゃった感じです。
 これが競合状態と言われる状態です。

競合状態を解決する同期システム

 競合状態を解決するためにはどうしたらいいでしょうか。例えばチャンネル争いで例えるとリモコンを一つだけ準備して、それを独占したらよいのです。
 もう少しかしこまっていうと、そのオブジェクトの独占権を1スレッドだけに渡すことで可能になります。例えば次のようになります。

def Increment(self):
  Monitor.Enter(self)

  if self.count <10 :
    Thread.Sleep(100)
    self.count = self.count +1
    print self.count

  Monitor.Exit(self)

 countを使う処理部分をMonitorクラスのEnterメゾットとExitメゾットで挟みます。感覚でいうと一人しか入ることが許されていないモニタールームに入って、処理して、出てくる感じです。一人は入っている間は別の人はモニタルーム前で待っています。
 ではMonitorクラスの説明です。

 Monitor.Enter(占有するオブジェクト)
  処理するコード
 Monitor.Exit(占有するオブジェクト)

 上記のように使います。今回は占有するオブジェクトはself(=MyClassクラス)自身になります。countはMyClassのアトリビュートであるため、MyClassを占有すれば、同時に占有されることになります。今回MyClassのアトリビュートではなく、グローバル変数としてcountを宣言し、Monitorクラスに渡すことも考えたのですが、うまく動きませんでした。
どうもMonitorクラスは、クラスを渡すように設計されているようです。
 コード全体では次のようになります。

import System
from System.Threading import *

class MyClass:
  def __init__(self):
    self.count = 0

  def Increment(self):
    Monitor.Enter(self)
    if self.count <10 :
      Thread.Sleep(100)
      self.count = self.count +1
      print self.count
    Monitor.Exit(self)

  def start(self):
    for i in xrange(100):
      th = Thread(ThreadStart(self.Increment))
      th.Start()

a = MyClass()

a.start()

raw_input()

出力画面:
 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
やっと予定通りの出力ができました。今回は単純なスレッドなので問題ありませんが、複雑に処理を実施した際、モニタールームの中で固まってしまうと別のスレッドも処理できなくて困ってしまいます。なので例外処理を使ってより安全に記述すると次のようになります。

import System
from System.Threading import *

class MyClass:
  def __init__(self):
    self.count = 0

  def Increment(self):
    Monitor.Enter(self)
    try:
      if self.count <10 :
        Thread.Sleep(100)
        self.count = self.count +1
        print self.count
    finally:
      Monitor.Exit(self)

  def start(self):
    for i in xrange(100):
      th = Thread(ThreadStart(self.Increment))
      th.Start()

a = MyClass()

a.start()

raw_input()

出力画面:
 1
 2
 3
 4
 5
 6
 7
 8
 9
 10