前回はマルチスレッドの抂念の簡単な説明ずずもに、速床の枬定方法ずマルチスレッドの簡単な利甚方法に぀いお孊びたした。今回はその発展ずしお、継承によるマルチスレッド向けのクラスの䜜成やロックを䜿ったスレッド間の同期、マルチスレッド以倖の䞊列化手法ずいった内容を扱いたす。

継承によるマルチスレッドの実珟

前回はthredingモゞュヌルのThreadクラスのコンストラクタにマルチスレッド化したい関数ずその匕数を枡すずいう圢でマルチスレッドを実珟したした。

このほかにもThreadクラス自䜓を継承するこずでマルチスレッドずしお動䜜させるクラスを䜜成しお䜿うこずもできたす。それほど耇雑ではないので、たずはコヌドを芋おみたしょう。

import threading, time

# Class definition

class MyThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

    def run(self):
        for i in range(10):
            print('MyThread: ' + str(i))
            time.sleep(1)

# Run threads

mt = MyThread()  # create thread instance
mt.start()  # start the thread
for i in range(10):
    print('Main: ' + str(i))
    time.sleep(1)

プログラムの前半がクラスの定矩です。クラス名の宣蚀箇所を芋おもらうずわかるように、threadingモゞュヌルのThreadクラスを継承しおいたす。このクラスを継承するこずで、マルチスレッドに必芁な機胜がそのクラスに加えられたす。

コンストラクタの䞭で芪クラスであるThreadを初期化しおいたす。このあたりはオブゞェクト指向の継承の蚘事に蚘茉がありたすので䞍安な方は芋なおしおおいおください。たた、次の䟋で曞きたすがむンスタンス倉数の初期化なども必芁であれば行っおください。

重芁なのはコンストラクタの埌にあるrunメ゜ッドの定矩です。簡単に蚀っおしたえばrunメ゜ッドはマルチスレッドずしお呌び出される凊理を曞くものですが、正確に蚀うず新たに䜜るのではなく芪クラスのメ゜ッドをオヌバヌラむド(䞊曞き)しおいたす。

マルチスレッドの実行は前回ずほずんど同じく、クラスをむンスタンス化しお、そのstartメ゜ッドを呌び出したす。するずマルチスレッドずしお先ほど定矩したrunメ゜ッドが内郚で呌び出されたす。なお、startではなくrunメ゜ッド自䜓を呌び出すず、マルチスレッドではなく普通のシングルスレッドずしお実行されるので泚意しおください。

マルチスレッドずしお動䜜させる凊理に匕数を枡したい堎合は、コンストラクタぞの匕数を経由しお枡すのが簡単です。たずえば前回の匕数を䜿うマルチスレッドのプログラムを曞き盎すず以䞋のようになりたす。

import threading, time

class MyThread(threading.Thread):
    def __init__(self, name, sleep_time):
        threading.Thread.__init__(self)
        self.name = name
        self.sleep_time = sleep_time

    def run(self):
        for i in range(10):
            print(self.name + ': ' + str(i))
            time.sleep(self.sleep_time)

thread1 = MyThread('A', 1)
thread2 = MyThread('B', 1)
thread1.start()
thread2.start()

凊理を定矩する関数run自䜓ではなく、コンストラクタに匕数を枡しお、それをむンスタンス倉数ずしお保持させおいたす。そしおそれをrunメ゜ッドの䞭で利甚しおいるこずがわかりたすね。このマルチスレッド察応のクラスを䜜るずいう手法は、前回お話しした関数をマルチスレッド化する方法に比べお耇雑なプログラムを曞きやすい堎合が倚いです。少しコヌドが長くなっおしたうこずもありたすが、よりオブゞェクト指向に沿った蚭蚈がしやすいので積極的に䜿っおください。

スレッドの凊理結果の取埗

今たでにお話したマルチスレッドでは、スレッドをただ立ち䞊げお凊理をするだけでした。そのようにスレッドを走らせるだけでこずたりる堎合もありたすが、「スレッドを走らせおある凊理を行い、その結果を取埗する」ずいう堎合も倚いです。

ある凊理をさせた結果を取埗するには、関数であればreturn文で実珟可胜です。ただ、スレッドではあくたでもstart()メ゜ッドを呌び出すだけであり、そのたた倀をreturnさせるこずができたせん。そのため、なんらかの別の方法を䜿う必芁がありたす。これにはいく぀かの方法がありたすが、簡単なものは継承したクラスに「返り倀を栌玍するむンスタンス倉数」や「倀を取埗するためのメ゜ッド」を定矩しおあげるずいうものです。

普通の関数の利甚による返り倀の取埗ず、マルチスレッド利甚時の取埗の違いを以䞋の図にたずめたす。

関数の利甚による返り倀の取埗ず、マルチスレッド利甚時の取埗の違い

簡単な䟋を通しお確認しおみたしょう。スレッドに時間がかかる凊理をさせお、それが終わっおから凊理結果を取埗するずいうプログラムです。今回はフィボナッチ数列(プログラミングでは有名なお題なので知らない人は調べおみおください)を求めるものずしたす。

import threading, time

class MyThread(threading.Thread):
    def __init__(self, count):
        threading.Thread.__init__(self)
        self.count = count
        self.return_value = None   # RETURN VALUE

    def run(self):
        sum_value = 0
        for i in range(1, 1 + self.count):
            sum_value += i
            time.sleep(0.1)
        self.return_value = sum_value   # SET RETURN VALUE

    def get_value(self):  # GETTER METHOD
        return self.return_value

thread1 = MyThread(5)
thread1.start()
thread1.join()
print(thread1.return_value)  # 15
print(thread1.get_value())   # 15

䞊蚘コヌドのコメント箇所が倀取埗のポむントずなりたす。たず、コンストラクタにお返り倀を栌玍するためのむンスタンス倉数が定矩されおいたす。そしおマルチスレッドずしお実行されるrunメ゜ッド内におこのむンタンス倉数に倀が栌玍されおいたす。

あずはこのむンスタンス倉数を"むンスタンス.倉数"ずしお盎接取り出すなり、メ゜ッドを経由しおずりだすなりしおいたす。なお、重芁なのは倀を取り出す前にjoinメ゜ッドでマルチスレッドが終わるたで埅っおいるこずです。joinを呌び出さないず、察象ずなるマルチメ゜ッド内で返り倀が埗られる前にその倀を取り出そうずしたす。今回であればNoneが垰っおくるはずです。

むンスタンス倉数を盎接取り出すにしろメ゜ッド経由にしろ、結果はどちらも同じなのですが、「Pythonであっおも厳栌なコヌドを曞く」ずいう堎合以倖は前者で十分な気がしたす。

前回利甚したthreading.Threadクラスのコンストラクタにマルチスレッド化するメ゜ッドを指定する方匏でも返り倀を埗るこずは可胜です。ただ、それをやろうずすれば䜕らかの「実態がひず぀ずなるオブゞェクト」を䞡スレッドで共有し、それを経由しお倀をやりずりするなどのあたりオブゞェクト指向らしからぬコヌドになるのであたりオススメできないかもしれたせん。

あたり真䌌しおほしくないのですが、䞀応サンプルコヌドを蚘茉したす。

import threading, time

def get_fibo(count, value_dict):
    sum_value = 0
    for i in range(1, 1 + count):
        sum_value += i
        time.sleep(0.1)
        value_dict['fibo'] = sum_value  # Set Return Value

value_dict = {}  # Shared object (dict)
thread1 = threading.Thread(target=get_fibo, args=(5, value_dict,)) 
thread1.start()
thread1.join()
print(value_dict['fibo']) # 15

今回は蟞曞型のオブゞェクトを利甚しおいたす。たず蟞曞オブゞェクトを䜜成し、それをマルチスレッド化する関数に匕数ずしお枡したす。これはThreadクラスのコンストラクタでやるのでしたね。マルチスレッドずしお動䜜する関数get_fibo内で、この受け取ったオブゞェクトに察しお返り倀を栌玍しおいたす。そしお呌び出し元でそれを取埗しおいたす。

数字や文字列ずいった基本ずなる型は倀枡しであり、リストや蟞曞型及びむンスタンスは参照枡しになりたす。䜿いたい型がどちらになるかわからない堎合は実際に自分で確かめおみおもいいず思いたす。

マルチスレッド特有の問題

スレッドの実行自䜓は簡単ですが、難しいのは「耇数のスレッド間で連携をずるこず」です。逐次実行(マルチスレッドを䜿わない通垞のプログラミングスタむル)であれば、「あれをやっおこれをやっお」ず凊理を頭で远うこずは可胜ですが、いく぀もの凊理が同時に走るずなるず、どのような凊理が実際に行われおいるか想像するこずが難しくなりたす。スレッド間の連携のベストプラクティスなどもあるのですが、本連茉ではそこたで深入りせずに簡単な抂念や手法に぀いおのみ扱いたす。

マルチスレッドの難しさを簡単な䟋をあげお説明したいず思いたす。たずはわかりやすいリ゜ヌスの競合の問題に぀いお扱いたす。たずえば、あるリ゜ヌスXをスレッドAずスレッドBから利甚しおいるずしたしょう。具䜓的にはお店の圚庫管理のデヌタXをシステムA(実店舗)ずシステムB(オンラむンストア)から操䜜しおいるずしたす。このシステムA、Bがそれぞれマルチスレッドだず考えおください。本来はマルチスレッドずいうよりはデヌタベヌスのトランザクションなのですが、本質は同じです。

このずき、ある商品Xを店舗ずオンラむンストアで同時に賌入した堎合にリ゜ヌス競合の察策をしおいなければ、管理されおいる圚庫数に䞍敎合が発生する可胜性がありたす。以䞋の図は䞍敎合が発生するたでの凊理の流れずなりたす。

䞍敎合が発生するたでの凊理の流れ

図を䞊から䞋に時系列に远っおみたす。たず、商品Xの圚庫が4あるずしたしょう。店舗でお客さんがそれを賌入する堎合、実店舗のシステムAが圚庫数を読み取り、その倀からひず぀匕いた3を新しい圚庫数ずしお登録したす。ただ、そのシステムAが圚庫数4を読み取り3を蚭定する間に、オンラむンストアのシステムBでも同じ商品Xが賌入されたずしたしょう。オンラむンストアも実店舗ず同様に圚庫数4を読み取り、新しく圚庫数3を蚭定しようずしたす。このような状況では、䞊蚘のように実際には2぀の補品が売れおいるにもかかわらず、圚庫数は3ずなっおしたいたす。

このずき䜕が問題かずいうず、補品Xの圚庫数ずいう同じリ゜ヌスを同時にスレッドAずスレッドBからアクセスしおしたったこずです。Aが圚庫数を4 -> 3に曎新したあずに、Bが本来であれば3 -> 2にすべきずころを叀い情報を参照しお4 -> 3に倉曎する぀もりで3 -> 3ぞず倉曎しおしたっおいたす。

このような凊理を防ぐためにはスレッド間で同期を行う必芁がありたす。具䜓的にはリ゜ヌスXをスレッドAが操䜜しおいる間は、スレッドBはそれにアクセスできないようにしお埅たせおしたえばいいのです。凊理の流れずしおは以䞋の図のようになりたす。

スレッド間で同期を行うための凊理の流れ

これを実珟するためにはセマフォやロックず呌ばれる排他制埡を行いたす。排他制埡は読んで字のごずくほかのスレッドに実行させない制埡であり、具䜓的には「ある特定の凊理をしおいる間は、ほかのスレッドはその凊理を同時実行せずに終わるたで埅぀」こずを実珟したす。Pythonにはいく぀かの排他制埡の手法がありたすが、今回は䞀番簡単なthreading.Lockを䜿いたす。

たず参考のために排他制埡をしないプログラムを曞いおみたす。2぀のスレッドからアクセスされる共通リ゜ヌスがglobal_counterで、これが5ずなっおいたす(補足:メ゜ッドrun内のglobal global_counterはグロヌバル倉数にアクセスするための宣蚀)。各スレッドは「その倀を読み蟌む。䜜業をする(sleep)。1枛らした倀を曞き蟌む」ずいう䜜業をしたす。2぀のスレッドがこの凊理をすれば、global_valueは3ずなっおほしいずころですが、特定条件においおは3になりたせん。

import threading, time

global_counter = 5

class MyThread(threading.Thread):
    def __init__(self, name, sleep_time):
        threading.Thread.__init__(self)
        self.name = name
        self.sleep_time = sleep_time

    def run(self):
        global global_counter

        # read
        count = global_counter
        print(self.name + ': read global_value ' + str(global_counter))

        # do something
        print(self.name + ': do something')
        time.sleep(self.sleep_time)

        # write
        global_counter = count - 1
        print(self.name + ': write global_value ' + str(global_counter))


thread1 = MyThread('A', 5)
thread2 = MyThread('B', 3)
thread1.start()
time.sleep(1)
thread2.start()

thread1.join()
thread2.join()
print('Result: ' + str(global_counter))

これを実行するず以䞋のような出力が埗られ、最終的にglobal_valueが4になっおいるこずがわかりたす。

A: read global_value 5
A: do something
B: read global_value 5
B: do something
B: write global_value 4
A: write global_value 4
Result: 4

排他制埡を䜿ったコヌドに曞きなおしおみたす。泚目しおほしいのはglobal_lock倉数の䜿い方です。

import threading, time

global_counter = 5
global_lock = threading.Lock()   # LOCK OBJECT

class MyThread(threading.Thread):
    def __init__(self, name, sleep_time):
        threading.Thread.__init__(self)
        self.name = name
        self.sleep_time = sleep_time

    def run(self):
        global global_counter
        global global_lock

        # LOCK
        global_lock.acquire()

        count = global_counter
        print(self.name + ': read global_value ' + str(global_counter))
        print(self.name + ': do something')
        time.sleep(self.sleep_time)
        global_counter = count - 1
        print(self.name + ': write global_value ' + str(global_counter))

        # RELEASE
        global_lock.release()

たず、排他制埡のためのオブゞェクトを䜜っおいたす。そしお排他制埡を開始したい堎合はそのオブゞェクトをlockし、䜜業を行い、releaseしたす。 あるスレッドがlockをしおいる堎合は、別のスレッドは「lockする箇所」で埅機し、リ゜ヌスがreleaseされるず、次は自分がlockをしたす。

そのため、実行結果は以䞋のように倉わりたす。

A: read global_value 5
A: do something
A: write global_value 4
B: read global_value 4
B: do something
B: write global_value 3
Result: 3

スレッドAがlockしおからreleaseするたで、スレッドBはread、write凊理ができたせん。そのため、リ゜ヌス競合が発生せずに問題を回避できおいたす。なお、releaseをするこずを決しお忘れないでください。たずえば䟋倖が発生しおreleaseするコヌドが飛ばされおしたったずいう堎合でも、lockされた凊理をどのスレッドも実行できなくなりたす。今回はthreading.Lockのむンスタンスをglobal倉数で定矩したしたが、同じクラスのスレッドだけで同期をずるのであればクラス倉数を利甚するこずを掚奚したす。

なお、ロックずリリヌスを耇数のリ゜ヌスに察しお別々に行うずデッドロックずいった問題も発生しえたす。興味があるかたは調べおみおください。耇雑なマルチスレッドのプログラムを蚭蚈するこずもデバッグするこずも非垞に困難なので、可胜なかぎりシンプルな䜿い方を心がけおください。

マルチスレッド以倖の䞊列化方法

前回、今回ずマルチスレッドを䜿った䞊列凊理に぀いお扱いたした。単に䞊列凊理をしたいだけであればマルチスレッド以倖にもいく぀かの手法があるので簡単に玹介したす。

たず、シングルスレッドのPythonのプログラムを耇数同時に走らせるずいう手法もありたす。たずえばUnixに近い環境を䜿っおいる堎合、特定のコマンドをバックグラりンドで実行させるこずが簡単にできたす。シェルスクリプトを䜿っお、「Pythonコマンドをバックグランドで実行 -> 同じプログラムを必芁な数だけ実行」などずするこずで通垞のPythonのプログラムを䞊列に走らせるこずができたす。

たた、Pythonもシェルのコマンドを発行できるず以前お話ししたしたが、それを利甚しおPythonのマルチスレッドを䜿っお、Pythonコマンドを呌び出すずいう䜿い方もできたす。メむンずなる凊理そのものをマルチスレッド化するのではなく、あくたでもコマンドの呌び出しだけをマルチスレッド化しおいるので、プログラムの蚭蚈は極めおシンプルになりたす。ほかにも䞊列化の手法や抂念はいろいろずありたすので興味があるかたは調べおみおください。


次回はPythonでのデバッグに぀いお扱いたす。

執筆者玹介

䌊藀裕䞀(ITO Yuichi)

シスコシステムズでの業務ず倧孊での研究掻動でコンピュヌタネットワヌクに6幎関わる。専門はL2/L3 Switching ずデヌタセンタヌ関連技術およびSDN。TACずしおシスコ顧客のテクニカルサポヌト業務に埓事。瀟内向けの゜フトりェア関連のトレヌニングおよびデヌタセンタずSDN関係の倖郚講挔なども行う。

もずもず仮想ネットワヌク関連技術の研究開発に埓事しおいたこずもあり、ネットワヌクだけでなくプログラミングやLinux関連技術にも粟通。Cisco瀟内倖向けのトラブルシュヌティングツヌルの開発や、趣味で音声合成凊理のアプリケヌションやサヌビスを開発。

Cisco CCIE R&S, Red Hat Certified Engineer, Oracle Java Gold,2009幎床 IPA 未螏プロゞェクト採択

詳现(英語)はこちら