2017/08/27

【Python】マルチスレッドでの排他制御

どうも。

とあるバグだらけのVB.NETで書かれたコードを引継ぐことになったのでマルチスレッドをおさらい…。 orz

今回は、あえてPythonでコードを書いてPythonのお勉強。 個人的にはVB.NETを脱却してC#やPythonを使いこなせるようになりたい。

(ちょっと本題とは外れるけど、個人的にはVB.NETを使い続けるメリットはもう無いと思っている。C#で代替できるし、むしろASP.NETを使うならC#のほうが良いだろうし。.NET CoreやMono DevelopなんかもC#のほうがメインだし。)


目次

上記の記事はとても分かりやすい。 記事中に書かれているC#やVB.NETのコードをPythonで書きおこしてみた。

「List1 データの不整合が発生する可能性のあるC#のサンプル・プログラム(List1.cs)」の部分をPython3 (Python3.5.1)で書いてみた。

# -*- coding: utf-8 -*-
import time
from threading import Thread

class Bank(object):
    """
    預金残高(balance)を保持するBankクラス
    """
    
    def __init__(self):
        self.__balance = 1000
        
    @property
    def balance(self):
        return self.__balance
        
    @balance.setter
    def balance(self, value):
        self.__balance = value
        
    @balance.deleter
    def balance(self):
        del self.__balance

class AtmThread(Thread):
    """
    預金の出し入れを行うスレッドクラス
    スレッドを使用している。
    """
    
    def __init__(self, name, bank):
        Thread.__init__(self)
        self.__name = name
        self.__bank = bank
        
    def run(self):
        self.thread_method()

    def thread_method(self):
        balance = self.__bank.balance
        time.sleep(1) # わざと競合を起こすため
        self.__bank.balance = balance + 200
        print("%s:balance + 200 = %s" % (self.__name, balance + 200))

def main():
    
    bank = Bank()
    
    atmA = AtmThread("A", bank)
    atmA.start()
    
    atmB = AtmThread("B", bank)
    atmB.start()

if __name__=='__main__':
    main()

オンラインの実行環境を使うとすぐにコードを試せます。

多分、ほとんどの場合、実行結果は以下のようになってしまうはず。

A:balance + 200 = 1200
B:balance + 200 = 1200

銀行(Bank)のインスタンスがあって、スレッドAとスレッドBがそれぞれATM(AtmThread)で200円ずつ入金している場面。

本来の意図としては以下のように動作させようとしている。

  1. スレッドAがBankの残高をbalanceに退避

    (balance = self.__bank.balance # balance は 1000)

  2. スレッドAが200円入金分を加算

    (self.__bank.balance = balance + 200 # self.__bank.balance は 1200)

  3. スレッドBがBankの残高をbalanceに退避

    (balance = self.__bank.balance # balance は 1200)

  4. スレッドBが200円入金分を加算

    (self.__bank.balance = balance + 200 # self.__bank.balance は 1400)

でも実際にはスレッドで並列処理しているのでタイミングによっては以下のような動作になる。

  1. スレッドAがBankの残高をbalanceに退避

    (balance = self.__bank.balance # balance は 1000)

  2. スレッドBがBankの残高をbalanceに退避

    (balance = self.__bank.balance # balance は 1000)

  3. スレッドAが200円入金分を加算

    (self.__bank.balance = balance + 200 # self.__bank.balance は 1200)

  4. スレッドBが200円入金分を加算

    (self.__bank.balance = balance + 200 # self.__bank.balance は 1200)

では不整合を起こさないようにするためにはどうすれば良いか。

抽象的な言い方をすると、複数のスレッドが共有している資源(リソース)に対して処理を行うスレッドを1つに限定すれば不整合を防ぐことができる。

今回の例で言うと、下記の部分(self.__bank.balanceの値の読み取りと上書きの部分)を必ず1つのスレッドのみが処理を行うようにすれば不整合を防ぐことができる。

balance = self.__bank.balance
time.sleep(1) # わざと競合を起こすため
self.__bank.balance = balance + 200

参考:クリティカルセクションとは - IT用語辞典

threading.Lockで排他制御ができる。

上記の記事に載っているList2のスレッドセーフのコード(C#/VB.NET)を真似てPythonで書いてみた。

# -*- coding: utf-8 -*-
import time
from threading import Thread, Lock

class Bank(object):
    """
    預金残高(balance)を保持するBankクラス
    """
    
    def __init__(self):
        self.__balance = 1000
        self.__lock = Lock()
        
    def add_balance(self, money):
        with self.__lock:
            balance_tmp = self.__balance
            time.sleep(1)
            self.__balance = balance_tmp + money
            return self.__balance


class AtmThread(Thread):
    """
    預金の出し入れを行うスレッドクラス
    """
    
    def __init__(self, name, bank):
        Thread.__init__(self)
        self.__name = name
        self.__bank = bank
        
    def run(self):
        self.thread_method()
    
    def thread_method(self):
        result = self.__bank.add_balance(200)
        print("%s:bank.Balance + 200 = %s" % (self.__name, result))

        
def main():
    bank = Bank()
    
    atmA = AtmThread("A", bank)
    atmA.start()
    
    atmB = AtmThread("B", bank)
    atmB.start()

if __name__=='__main__':
    main()

実行結果は以下の通りになる。

A:bank.Balance + 200 = 1200
B:bank.Balance + 200 = 1400

実行してみると分かるかもしれないけど、少しパフォーマンスが低下する。 排他制御を行う部分は1つのスレッドしか処理ができないから、適切に lock をかける部分を見極めないとマルチスレッド化している意味がなくなる。

2005年に公開された上記の記事のList2のコードを見ていて1点気付いたので引用してみる。

// 預金残高(balance)を保持するBankクラス
class Bank
{
  private object lockObject = new object();

  private int balance = 1000;

  public int AddBalance(int money)
  {
    lock (lockObject)
    {
      int balanceTmp = balance;
      Thread.Sleep(1000);
      balance = balanceTmp + 200;
      return balance;
    }
  }
}
引用元:連載.NETマルチスレッド・プログラミング入門:第3回 マルチスレッドでデータの不整合を防ぐための排他制御 ― マルチスレッド・プログラミングにおける排他制御と同期制御(前編) ― (2/3) - @IT List2 スレッドセーフにしたList1のBankクラス(C#)

おわかりいただけただろうか。

AddBalanceメソッドの引数にどんな金額を渡しても入金される金額は200円…(2017/08/27現在)。

まぁ、些細な間違いだね。



[広告]

Amazon

関連記事

スポンサーリンク

スポンサーリンク

スポンサーリンク

コメント

非公開コメント

No title

責任取れない意味のないコードを乗せるのは辞めてくれ。かなり迷惑

Re: No title

> 責任取れない意味のないコードを乗せるのは辞めてくれ。かなり迷惑

管理人です。
このコメントをご覧になるか分かりませんが、
具体的にどのような点でご迷惑をお掛けしているのか、
今後同じ轍を踏まないためにもご教示いただけると幸いです。