Python3 マルチスレッド

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

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

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

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

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

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

スレッドは次のように分類できます:

  • カーネルスレッド:オペレーティングシステムのカーネルによって作成および取り消されます。
  • ユーザースレッド:カーネルのサポートを必要とせず、ユーザープログラム内で実装されるスレッドです。

Python3 のスレッドでよく使われる2つのモジュールは次のとおりです:

  • _thread
  • threading(推奨)

thread モジュールは非推奨になりました。ユーザーは threading モジュールを代わりに使用できます。そのため、Python3 では "thread" モジュールを使用できません。互換性のために、Python3 は thread を "_thread" と改名しました。


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

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

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

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

パラメータの説明:

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

例

#!/usr/bin/python3

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: スレッドを起動できません")

while 1:
   pass

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

Thread-1: Wed Jan  5 17:38:08 2022
Thread-2: Wed Jan  5 17:38:10 2022
Thread-1: Wed Jan  5 17:38:10 2022
Thread-1: Wed Jan  5 17:38:12 2022
Thread-2: Wed Jan  5 17:38:14 2022
Thread-1: Wed Jan  5 17:38:14 2022
Thread-1: Wed Jan  5 17:38:16 2022
Thread-2: Wed Jan  5 17:38:18 2022
Thread-2: Wed Jan  5 17:38:22 2022
Thread-2: Wed Jan  5 17:38:26 2022

上記のプログラムを実行後、ctrl-c を押して終了できます。


スレッドモジュール

Python3 は、_thread と threading という2つの標準ライブラリを通じてスレッドをサポートします。

_thread は低レベルの原始的なスレッドと単純なロックを提供しますが、threading モジュールと比較すると機能はかなり限られています。

threading モジュールは、_thread モジュールのすべてのメソッドに加えて、次のメソッドも提供します:

  • threading.current_thread():現在のスレッド変数を返します。
  • threading.enumerate():実行中のスレッドを含むリストを返します。実行中とは、スレッドが起動後、終了前のことです。起動前と終了後のスレッドは含まれません。
  • threading.active_count():実行中のスレッド数を返します。len(threading.enumerate()) と同じ結果になります。
  • threading.Thread(target, args=(), kwargs={}, daemon=None):
    • 作成Threadクラスのインスタンス。
    • target:スレッドが実行するターゲット関数。
    • args:ターゲット関数の引数。タプル形式で渡されます。
    • kwargs:ターゲット関数のキーワード引数。辞書形式で渡されます。
    • daemon:スレッドがデーモンスレッドであるかどうかを指定します。

threading.Thread クラスは次のメソッドと属性を提供します:

  1. __init__(self, group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None):

    • 初期化Threadオブジェクト。
    • group:スレッドグループ。現在は未使用で、将来の拡張のために予約されています。
    • target:スレッドが実行するターゲット関数。
    • name:スレッドの名前。
    • args:ターゲット関数の引数。タプル形式で渡されます。
    • kwargs:ターゲット関数のキーワード引数。辞書形式で渡されます。
    • daemon:スレッドがデーモンスレッドであるかどうかを指定します。
  2. start(self):

    • スレッドを起動します。スレッドの ... を呼び出します。run()メソッド。
  3. run(self):

    • スレッドはこのメソッド内で実行するコードを定義します。
  4. join(self, timeout=None):

    • スレッドの終了を待機します。デフォルトでは、join()呼び出されたスレッドが終了するまでブロックし続けます。もし ... を指定した場合、timeoutパラメータを指定すると、最大で ... 待機します。timeout秒。
  5. is_alive(self):

    • スレッドが実行中かどうかを返します。スレッドが起動済みでまだ終了していない場合は ... を返します。Trueそれ以外の場合は ... を返します。False。
  6. getName(self):

    • スレッドの名前を返します。
  7. setName(self, name):

    • スレッドの名前を設定します。
  8. ident属性:

    • スレッドの一意の識別子。
  9. daemon属性:

    • スレッドのデーモンフラグ。デーモンスレッドであるかどうかを示します。
  10. isDaemon()メソッド:

簡単なスレッドの例:

例

import threading
import time

def print_numbers():
    for i in range(5):
        time.sleep(1)
        print(i)

# スレッドを作成
thread = threading.Thread(target=print_numbers)

# スレッドを起動
thread.start()

# スレッドの終了を待機
thread.join()

出力結果は次のとおりです:

0
1
2
3
4

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

threading.Thread を直接継承して新しいサブクラスを作成し、インスタンス化後に start() メソッドを呼び出して新しいスレッドを起動できます。つまり、スレッドの run() メソッドを呼び出します:

例

#!/usr/bin/python3

import threading
import time

exitFlag = 0

class myThread (threading.Thread):
    def __init__(self, threadID, name, delay):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.delay = delay
    def run(self):
        print ("スレッド開始:" + self.name)
        print_time(self.name, self.delay, 5)
        print ("スレッド終了:" + self.name)

def print_time(threadName, delay, counter):
    while counter:
        if exitFlag:
            threadName.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()
thread1.join()
thread2.join()
print ("メインスレッド終了")

上記のプログラムの実行結果は次のとおりです:

开始线程:Thread-1
开始线程:Thread-2
Thread-1: Wed Jan  5 17:34:54 2022
Thread-2: Wed Jan  5 17:34:55 2022
Thread-1: Wed Jan  5 17:34:55 2022
Thread-1: Wed Jan  5 17:34:56 2022
Thread-2: Wed Jan  5 17:34:57 2022
Thread-1: Wed Jan  5 17:34:57 2022
Thread-1: Wed Jan  5 17:34:58 2022
退出线程:Thread-1
Thread-2: Wed Jan  5 17:34:59 2022
Thread-2: Wed Jan  5 17:35:01 2022
Thread-2: Wed Jan  5 17:35:03 2022
退出线程:Thread-2
退出主线程

スレッド同期

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

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

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

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

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

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

このように処理することで、リストを出力するときは全部 0 か全部 1 のどちらかになり、半分 0 で半分 1 という困った事態は再び発生しません。

例

#!/usr/bin/python3

import threading
import time

class myThread (threading.Thread):
    def __init__(self, threadID, name, delay):
        threading.Thread.__init__(self)
        self.threadID = threadID
        self.name = name
        self.delay = delay
    def run(self):
        print ("スレッド開始: " + self.name)
        # ロックを取得してスレッドを同期する
        threadLock.acquire()
        print_time(self.name, self.delay, 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 ("メインスレッド終了")

上記のプログラムを実行すると、出力結果は次のとおりです:

开启线程: Thread-1
开启线程: Thread-2
Thread-1: Wed Jan  5 17:36:50 2022
Thread-1: Wed Jan  5 17:36:51 2022
Thread-1: Wed Jan  5 17:36:52 2022
Thread-2: Wed Jan  5 17:36:54 2022
Thread-2: Wed Jan  5 17:36:56 2022
Thread-2: Wed Jan  5 17:36:58 2022
退出主线程

スレッド優先度キュー(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) キューに書き込み、timeoutは待機時間
  • Queue.put_nowait(item) は Queue.put(item, False) に相当
  • Queue.task_done() は、1つの作業を完了した後、Queue.task_done()関数がタスク完了のキューに信号を送信する
  • Queue.join() は、実際にはキューが空になるまで待ってから、他の操作を実行することを意味します

例

#!/usr/bin/python3

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 ("スレッドを開始:" + self.name)
        process_data(self.name, self.q)
        print ("スレッドを終了:" + 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 ("メインスレッドを終了")

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

开启线程:Thread-1
开启线程:Thread-2
开启线程:Thread-3
Thread-3 processing One
Thread-1 processing Two
Thread-2 processing Three
Thread-3 processing Four
Thread-1 processing Five
退出线程:Thread-3
退出线程:Thread-2
退出线程:Thread-1
退出主线程
その他の拡張