Python マルチスレッド

マルチスレッドは、複数の異なるプログラムを同時に実行するようなものです。マルチスレッド実行には次のような利点があります:

  • スレッドを使用すると、長時間を要するプログラム内のタスクをバックグラウンドで処理できます。
  • ユーザーインターフェースをより魅力的にできます。例えば、ユーザーがボタンをクリックして特定のイベント処理をトリガーすると、処理の進行状況を示すプログレスバーを表示できます。
  • プログラムの実行速度が向上する可能性があります。
  • ユーザー入力、ファイルの読み書き、ネットワークデータの送受信などの待機時間が発生するタスクでは、スレッドが役立ちます。このような場合、メモリ使用量などの貴重なリソースを解放できます。

スレッドは実行中、プロセスとはまだ違いがあります。各独立プロセスには、プログラム実行の入口、順次実行シーケンス、プログラムの出口があります。しかし、スレッドは単独では実行できず、アプリケーションに依存して存在する必要があり、アプリケーションが複数のスレッドの実行制御を提供します。

各スレッドには、スレッドのコンテキストと呼ばれる独自のCPUレジスタセットがあります。このコンテキストは、そのスレッドが前回実行されたときのCPUレジスタの状態を反映します。

命令ポインタとスタックポインタのレジスタは、スレッドコンテキスト内で最も重要な2つのレジスタです。スレッドは常にプロセスのコンテキスト内で実行され、これらのアドレスはすべて、スレッドを所有するプロセスのアドレス空間内のメモリを識別するために使用されます。

  • スレッドはプリエンプト(割り込み)される可能性があります。
  • 他のスレッドが実行されている間、スレッドは一時的に休止(スリープとも呼ばれます)することができます -- これがスレッドの譲歩です。

Pythonスレッドの学習を始める

Pythonでスレッドを使用する方法は2つあります:関数を使用する方法と、クラスを使用してスレッドオブジェクトをラップする方法です。

関数方式:threadモジュールのstart_new_thread()関数を呼び出して新しいスレッドを生成します。構文は以下の通りです:

thread.start_new_thread ( function, args[, kwargs] )

パラメータの説明:

  • function - スレッド関数。
  • args - スレッド関数に渡す引数。tuple型である必要があります。
  • kwargs - オプションのパラメータ。

例(Python 2.0+)

#!/usr/bin/python # -*- coding: UTF-8 -*- import thread import time # スレッド用の関数を定義する def print_time( threadName, delay): count = 0 while count < 5: time.sleep(delay) count += 1 print "%s: %s" % ( threadName, time.ctime(time.time()) ) # 2つのスレッドを作成する try: thread.start_new_thread( print_time, ("Thread-1", 2, ) ) thread.start_new_thread( print_time, ("Thread-2", 4, ) ) except: print "Error: unable to start thread" while 1: pass

上記のプログラムを実行すると、出力結果は次のようになります:

Thread-1: Thu Jan 22 15:42:17 2009
Thread-1: Thu Jan 22 15:42:19 2009
Thread-2: Thu Jan 22 15:42:19 2009
Thread-1: Thu Jan 22 15:42:21 2009
Thread-2: Thu Jan 22 15:42:23 2009
Thread-1: Thu Jan 22 15:42:23 2009
Thread-1: Thu Jan 22 15:42:25 2009
Thread-2: Thu Jan 22 15:42:27 2009
Thread-2: Thu Jan 22 15:42:31 2009
Thread-2: Thu Jan 22 15:42:35 2009

スレッドの終了は通常、スレッド関数の自然な終了に依存します。また、スレッド関数内でthread.exit()を呼び出すこともできます。これはSystemExit例外をスローして、スレッドを終了させる目的を達成します。


スレッドモジュール

Pythonは、threadとthreadingという2つの標準ライブラリを通じてスレッドをサポートします。threadは低レベルで原始的なスレッドと単純なロックを提供します。

threadingモジュールが提供するその他のメソッド:

  • threading.currentThread(): 現在のスレッド変数を返します。
  • threading.enumerate(): 実行中のスレッドを含むリストを返します。実行中とは、スレッドが起動後、終了前であることを指し、起動前および終了後のスレッドは含まれません。
  • threading.activeCount(): 実行中のスレッド数を返します。len(threading.enumerate())と同じ結果になります。

メソッドの使用に加えて、スレッドモジュールはスレッドを処理するためのThreadクラスも提供します。Threadクラスは以下のメソッドを提供します:

  • run():スレッドのアクティビティを表すために使用するメソッド。
  • start():スレッドのアクティビティを起動します。
  • join([time]):スレッドが終了するまで待機します。これは、join()メソッドが呼び出されたスレッドが終了するまで(正常終了または未処理の例外がスローされるまで)、あるいはオプションのタイムアウトが発生するまで、呼び出し元スレッドをブロックします。
  • isAlive():スレッドがアクティブかどうかを返します。
  • getName():スレッド名を返します。
  • setName():スレッド名を設定します。

Threadingモジュールを使用したスレッドの作成

Threadingモジュールを使用してスレッドを作成するには、threading.Threadを直接継承し、__init__メソッドとrunメソッドをオーバーライドします:

例(Python 2.0+)

#!/usr/bin/python # -*- coding: UTF-8 -*- import threading import time exitFlag = 0 class myThread (threading.Thread): # 親クラスthreading.Threadを継承 def __init__(self, threadID, name, counter): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counter def run(self): # 実行するコードをrun関数に記述します。スレッドは作成後にrun関数を直接実行します。 print "Starting " + self.name print_time(self.name, self.counter, 5) print "Exiting " + self.name def print_time(threadName, delay, counter): while counter: if exitFlag: (threading.Thread).exit() time.sleep(delay) print "%s: %s" % (threadName, time.ctime(time.time())) counter -= 1 # 新しいスレッドを作成 thread1 = myThread(1, "Thread-1", 1) thread2 = myThread(2, "Thread-2", 2) # スレッドを開始 thread1.start() thread2.start() print "Exiting Main Thread"

上記のプログラムの実行結果は以下の通りです;

Starting Thread-1
Starting Thread-2
Exiting Main Thread
Thread-1: Thu Mar 21 09:10:03 2013
Thread-1: Thu Mar 21 09:10:04 2013
Thread-2: Thu Mar 21 09:10:04 2013
Thread-1: Thu Mar 21 09:10:05 2013
Thread-1: Thu Mar 21 09:10:06 2013
Thread-2: Thu Mar 21 09:10:06 2013
Thread-1: Thu Mar 21 09:10:07 2013
Exiting Thread-1
Thread-2: Thu Mar 21 09:10:08 2013
Thread-2: Thu Mar 21 09:10:10 2013
Thread-2: Thu Mar 21 09:10:12 2013
Exiting Thread-2

スレッド同期

複数のスレッドが特定のデータを共同で変更する場合、予期しない結果が生じる可能性があります。データの正確性を保証するには、複数のスレッドを同期する必要があります。

ThreadオブジェクトのLockとRlockを使用すると、簡単なスレッド同期を実現できます。これらの2つのオブジェクトにはacquireメソッドとreleaseメソッドがあります。毎回1つのスレッドだけが操作できるようにする必要があるデータについては、その操作をacquireメソッドとreleaseメソッドの間に置くことができます。次のように:

マルチスレッドの利点は、複数のタスクを同時に実行できることです(少なくともそのように感じられます)。しかし、スレッドがデータを共有する必要がある場合、データの同期が取れない問題が発生する可能性があります。

次のような状況を考えてみましょう:リスト内のすべての要素が0であり、スレッド"set"が後ろから前へすべての要素を1に変更し、スレッド"print"が前から後ろへリストを読み取って出力するとします。

その場合、スレッド"set"が変更を開始したときに、スレッド"print"がリストを出力すると、出力が半分0、半分1になります。これがデータの非同期です。このような事態を避けるために、ロックの概念が導入されました。

ロックには2つの状態があります——ロック済みと未ロックです。スレッド(例えば"set")が共有データにアクセスするときは常に、まずロックを取得する必要があります。すでに他のスレッド(例えば"print")がロックを取得している場合は、スレッド"set"を一時停止させます。つまり同期ブロックです。スレッド"print"がアクセスを完了してロックを解放した後に、スレッド"set"を続行させます。

このような処理により、リストを出力するときはすべて0またはすべて1のどちらかになり、半分0、半分1という気まずい状況は再び発生しません。

例(Python 2.0+)

#!/usr/bin/python # -*- coding: UTF-8 -*- import threading import time class myThread (threading.Thread): def __init__(self, threadID, name, counter): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.counter = counter def run(self): print "Starting " + self.name # ロックを取得し、ロックの取得に成功するとTrueを返す # オプションのtimeoutパラメータを指定しない場合、ロックを取得するまでブロックし続ける # それ以外の場合、タイムアウト後はFalseを返す threadLock.acquire() print_time(self.name, self.counter, 3) # ロックを解放 threadLock.release() def print_time(threadName, delay, counter): while counter: time.sleep(delay) print "%s: %s" % (threadName, time.ctime(time.time())) counter -= 1 threadLock = threading.Lock() threads = [] # 新しいスレッドを作成 thread1 = myThread(1, "Thread-1", 1) thread2 = myThread(2, "Thread-2", 2) # 新しいスレッドを開始 thread1.start() thread2.start() # スレッドをスレッドリストに追加 threads.append(thread1) threads.append(thread2) # すべてのスレッドが完了するのを待機 for t in threads: t.join() print "Exiting Main Thread"

スレッド優先度キュー(Queue)

PythonのQueueモジュールには、同期されたスレッドセーフなキュークラスが用意されています。FIFO(先入れ先出し)キューのQueue、LIFO(後入れ先出し)キューのLifoQueue、優先度キューのPriorityQueueがあります。これらのキューはすべてロックプリミティブを実装しており、マルチスレッドで直接使用できます。キューを使用してスレッド間の同期を実現できます。

Queueモジュールの一般的なメソッド:

  • Queue.qsize() キューのサイズを返します
  • Queue.empty() キューが空の場合はTrueを返し、そうでない場合はFalseを返します
  • Queue.full() キューが満杯の場合はTrueを返し、そうでない場合はFalseを返します
  • Queue.full は maxsize のサイズと対応します
  • Queue.get([block[, timeout]]) キューを取得します。timeoutは待機時間です
  • Queue.get_nowait() は Queue.get(False) に相当します
  • Queue.put(item, block=True, timeout=None) キューに書き込みます。timeoutは待機時間です
  • Queue.put_nowait(item) は Queue.put(item, False) に相当します
  • Queue.task_done() は、1つの作業を完了した後、タスクが完了したことをキューにシグナルを送信します。
  • Queue.join() は実際にはキューが空になるまで待ってから、他の操作を実行することを意味します

例(Python 2.0+)

#!/usr/bin/python # -*- coding: UTF-8 -*- import Queue import threading import time exitFlag = 0 class myThread (threading.Thread): def __init__(self, threadID, name, q): threading.Thread.__init__(self) self.threadID = threadID self.name = name self.q = q def run(self): print "Starting " + self.name process_data(self.name, self.q) print "Exiting " + self.name def process_data(threadName, q): while not exitFlag: queueLock.acquire() if not workQueue.empty(): data = q.get() queueLock.release() print "%s processing %s" % (threadName, data) else: queueLock.release() time.sleep(1) threadList = ["Thread-1", "Thread-2", "Thread-3"] nameList = ["One", "Two", "Three", "Four", "Five"] queueLock = threading.Lock() workQueue = Queue.Queue(10) threads = [] threadID = 1 # 新しいスレッドを作成 for tName in threadList: thread = myThread(threadID, tName, workQueue) thread.start() threads.append(thread) threadID += 1 # キューを満たす queueLock.acquire() for word in nameList: workQueue.put(word) queueLock.release() # キューが空になるのを待機 while not workQueue.empty(): pass # スレッドに終了するタイミングを通知 exitFlag = 1 # すべてのスレッドが完了するのを待機 for t in threads: t.join() print "Exiting Main Thread"

上記のプログラムの実行結果:

Starting Thread-1
Starting Thread-2
Starting Thread-3
Thread-1 processing One
Thread-2 processing Two
Thread-3 processing Three
Thread-1 processing Four
Thread-2 processing Five
Exiting Thread-3
Exiting Thread-1
Exiting Thread-2
Exiting Main Thread
その他の拡張