Select Language

AI Technology Community

8.4、Pythonスレッド同期実現方式の詳細

スレッドが起動されると、自動的に実行されます。しかし、それらを同期して実行したい場合は、どうすればよいでしょうか?

簡単な例を挙げましょう。2つのスレッドAとBがあり、Aはネットワークからデータを読み取り、変数Xに保存し、Bは変数X内のデータを処理する役割を担っています。このとき、スレッドBはAと同期する必要があります。つまり、BはAから信号を受け取るまで、自分の仕事を開始することができません。同様に、Bがタスクを完了した後も、Aに通知し、変数X内のデータが処理されたことを知らせ、新しいデータをXに入れることができるようにする必要があります。

図1はこのプロセスを表しています:

スレッド同期
図1 スレッド同期


スレッド同期を実現する方法はたくさんあります。以下でそれぞれ紹介します。

1、thread.Lockスレッドロック

ロックを利用することで、排他的なリソースを取得することができます。たとえば、あるリソースAにロックLをかけると、そのリソースを使用するには、ロックLを取得する必要があります。このロックは、任意の時点で、ただ1つのスレッドのみが取得できることを保証します。他のスレッドがすでに別のスレッドによって取得されているロックを取得しようとする場合、ロックの所有者が自発的にロックを解放するまで待たなければなりません。

このロッククラスは、acquire()とrelease()の2つのインターフェース関数を提供しています。release()は、あるスレッドがタスクを完了し、他のスレッドが自分の仕事を開始できることを意味します。acquire()は、あるスレッドがある仕事を行う予定であり、開始できるようになったら通知してほしいことを意味します。

したがって、ある仕事を行うには、以下の3つのステップで構成することができます:

  • acquire():条件が整ったら、あるスレッドに通知する。

  • do_the work():条件が整ったので、仕事を開始する。

  • release():仕事が完了し、他のスレッドが仕事を開始できることを通知する。


では、先ほどの例に戻りましょう。2つのロックが必要です。1つは変数Xに書き込むためのもので、write_lockで表します。もう1つは変数Xを読み取るためのもので、read_lockで表します。変数Xに書き込むプロセスは以下の通りです:

write_lock.acquire()
X=var
read_lock.release()


変数Xを読み取るプロセスは以下の通りです:

read_lock.acquire()
var=X
write_lock.release()


以下は完全なコードです。

import sys, time                        # 時間ライブラリをインポート
if sys.version_info.major == 2:         # Python 2
import thread
else:                                   # Python 3
import _thread as thread               # 2つのロックを作成する。1つは読み取り用、もう1つは書き込み用
read_lock = thread.allocate_lock()
write_lock = thread.allocate_lock()
X = 0                                   # 変数X。2つのスレッド間で交換するデータを保存する
def write_thread_entry():               # 書き込みスレッドのエントリー関数
    global X, read_lock,  write_lock
    for i in range(2, 10, 1):
        write_lock.acquire()
        X = i
        read_lock.release()
def read_thread_entry():                # 読み取りスレッドのエントリー関数
    global X, read_lock, write_lock
    while True:
        read_lock.acquire()
        print("Processing X = %d" % X)
        write_lock.release()
def start_threads():                    # スレッドを起動する
    read_lock.acquire()                 # read_lockが占有されている状態
    t1 = thread.start_new_thread(write_thread_entry, tuple())
    t2 = thread.start_new_thread(read_thread_entry, tuple())
    time.sleep(5)
if __name__=='__main__':                # このスクリプトを実行している場合、インポートしているのではない
    start_threads()

実行結果は以下の通りです:

$ python lockDemo1.py         # スクリプトを実行する
Processing X = 2            # プログラムの20行目の出力
Processing X = 3            # 現在のXの値
Processing X = 4
Processing X = 5
Processing X = 6
Processing X = 7
Processing X = 8
Processing X = 9

2、threading.Lockスレッドロック

threadingパッケージにもLockクラスが含まれており、提供する関数はacquire()とrelease()で、これらの関数はthread.Lockクラスの関数と同じです。ただし、Lockを作成するためのallocate_lock()インターフェース関数は提供されておらず、コンストラクタを使用して自分で作成する必要があります。

以下のコードは、ほぼ同じ方法で先ほどの例を実現しています:

import sys, time
import threading                       # スレッドライブラリをインポート
read_lock = threading.Lock()
write_lock = threading.Lock()
X = 0                                  # 変数X。読み取りスレッドと書き込みスレッドが情報を交換するためのキャリア
def write_thread_entry():
    global X, read_lock,  write_lock
    for i in range(2, 10, 1):
        write_lock.acquire()
        X = i
        read_lock.release()
def read_thread_entry():
    global X, read_lock, write_lock
    while True:
        read_lock.acquire()
        print("Processing X = %d" % X)
        write_lock.release()
def start_threads():
    read_lock.acquire()         # read_lockが占有されている状態
    t1 = threading.Thread(target=write_thread_entry)
    t1.setDaemon(True)
    t1.start()
    t2 = threading.Thread(target=read_thread_entry)
    t2.setDaemon(True)
    t2.start()
    time.sleep(5)
if __name__=='__main__':
    start_threads()

実行結果は以下の通りです:

$ python lockDemo1.py
Processing X = 2          # プログラムの16行目の出力
Processing X = 3          # 現在のXの値を表示する
Processing X = 4
Processing X = 5
Processing X = 6
Processing X = 7
Processing X = 8
Processing X = 9

3、threading.RLock再入可能ロック

先ほど紹介したthreading.Lockには、あるthreading.Lockオブジェクトに対して、同じスレッド内でacquire()を2回繰り返し呼び出すと、ロック状態になってしまうという問題があります。

以下のコードは、この状況を示しています:

import sys, time
import threading
lock_obj1 = threading.Lock()            # ロックオブジェクトを作成する
def thread_entry():                     # 子スレッドのエントリー関数
    global lock_obj1                    # グローバル変数lock_obj1を使用する
    print("Child Thread: thread_entry() Is Running")
    lock_obj1.acquire()                         # 1回目のacquire()呼び出し。成功する
    print("Child Thread: acquire(1) Finished")
    lock_obj1.acquire()                         # 2回目のacquire()呼び出し。ブロックされる
            # 
post
  • 10

    item of content
プロセスはリソースを分配する単位であり、スレッドはオペレーティングシステムがスケジューリングできる最小の単位です。
通常、プロセスには少なくとも1つのスレッドが含まれ、複数のスレッドがある場合はその中にメインスレッドが含まれます。同じプロセス内のすべてのスレッドはシステムリソースを共有しますが、それぞれが独立したスタック、レジスタ環境、およびローカルストレージを持っています。
マルチスレッドの利点は、複数のタスクを同時に実行できることです。システムに複数の計算ユニットがある場合、複数のスレッドはそれぞれの計算ユニットで並行して動作することができ、これによりシステムの処理効率が大幅に向上します。
多くの場合、プロセスはスレッドよりも大きい単位であり、通常1つのプロセスは複数のスレッドを含むことができます。プロセスの隔離効果はスレッドよりも優れているため、マルチプロセスを使用するとマルチスレッドよりも安全です。ただし、マルチプロセスの欠点はマルチスレッドよりもスケジューリングが重く、効率が低いことです。