Pythonでマルチスレッド(並列実行)を実装する

힘센캥거루
2025년 7월 2일(수정됨)
61
python
Pythonでマルチスレッド(並列実行)を実装する-1

Pythonでコーディングしていると、tkinterのようなGUIを実装する際に問題に直面する場合があります。

ボタンを押して特定の関数を実行するとGUIも一緒に停止してしまうのです。

そこで、これをマルチスレッドで解決してみることにしました。

1. マルチスレッド? マルチプロセッシング?

Pythonでマルチスレッド(並列実行)を実装する-2

マルチスレッドは1つのプロセス内で複数のタスクフロー(スレッド)を同時に実行する技法です。

例えば、tkinterで作成したGUIがメインスレッドで動いているとき、ボタンを押して実行される関数が時間がかかるとGUIも一緒に止まってしまいます。

学校で勤務している人なら誰でも知っている「UNIV」(ユニブ)という入試プログラムがその例です。

Pythonでマルチスレッド(並列実行)を実装する-3

特定のウィンドウを開くと元のウィンドウが止まってしまい、GUI構成はユーザにとって非常に不便をもたらします。

このようなとき、ボタンクリックで実行される関数を別のスレッドで実行すると、GUIはメインスレッドで継続実行され、他のスレッドで関数が実行されるため画面が止まりません。

Pythonでマルチスレッド(並列実行)を実装する-4

一方、マルチプロセッシングは完全に別のプロセスを生成して実行する方式です。

スレッドは1つのプロセス内でメモリを共有しながら実行されますが、マルチプロセッシングはプロセスを複製して独立したメモリ空間で実行されます。

CPUコアを複数活用でき、計算量が多い作業には有利ですが、GUIプログラムでは別プロセス間通信が必要でスレッドほど簡単には使用できません。

区分

マルチスレッド

マルチプロセッシング

実行方式

1つのプロセス内の複数スレッド

複数の独立プロセス

メモリ

メモリ空間共有

メモリ空間独立(複製)

CPU活用

GIL(Global Interpreter Lock)のためCPUバウンド作業には制約

CPUコアを複数使用可能

GUIとの関係

GUI停止防止に適合

GUIとは別プロセスで通信が必要

使用例

tkinterボタンクリック後の関数実行

画像処理、大規模計算など

Pythonでマルチプロセッシングが可能な理由は、multiprocessingライブラリを利用してプロセスを複製あるいは新たに生成して実行できるからです。

しかしメモリはプロセスごとに独立しているため、データを共有するにはQueue, Pipe, 共有メモリなどを使う必要があります。

つまり、GUIプログラムのようにユーザインターフェイスが停止しないことが重要な場合はマルチスレッドを、大規模な計算作業にはマルチプロセッシングを適切に活用するのが良いでしょう。

2. マルチスレッドコード例

例えば以下のようなコードがあるとしましょう。

このコードは入力した数だけhello, hiを出力するコードです。

import time

def printHello(num):
    for i in range(num):
        print(f'hello-{i}')
        time.sleep(1)
        
def printHi(num):
    for i in range(num):
        print(f'hi-{i}')
        time.sleep(1)
        
printHello(3)
printHi(2)

下の行のようにコードを実行すると、helloが3回出た後にhiが2回出ます。

Pythonは同期実行されるため、1つの関数がすべて終了するまで次の関数は実行されず待機するからです。

hello-0
hello-1
hello-2
hi-0
hi-1

これをマルチスレッドで実装してみましょう。

printHelloとprintHiは上と同じで、コードの簡単な説明はコメントに記載しました。

import threading
import time

...
        
hello = threading.Thread(target=printHello, args=(3, ), daemon=True)
hello.start()
hi = threading.Thread(target=printHi, args=(2, ), daemon=True)
hi.start()
hello.join()
hi.join()
print('作業終了')

こう実行すると、helloとhiが1秒間隔で同時に出力されます。

そして最後のhello出力後、作業終了が出力されます。

hello-0hi-0
hello-1hi-1
hello-2
作業終了

ここでThreadのパラメーターの重要な点だけ挙げると以下の通りです。

daemonは通常trueにしておくと精神衛生が良いです。

オプション

説明

target

実行対象の関数

target=my_function

args

関数に渡すパラメーター(タプル)

args=("パラメーター1", 2)

daemon

デーモンスレッドかどうか。Trueでメインスレッド終了時に一緒に終了

daemon=True

3. threadingじゃなくて futuresを使えって?

threadingはそれほど難しくないが、daemonやjoinなどを指定する必要があります。

Python 3.2以降ではconcurrent.futuresというライブラリがサポートされており、これを使えばもう少し簡単に実装できます。

import time
from concurrent.futures import ThreadPoolExecutor

def printHello(num, name):
    for i in range(num):
        print(f'{name}-hello-{i}')
        time.sleep(1)

def printHi(num, name):
    for i in range(num):
        print(f'{name}-hi-{i}')
        time.sleep(1)

executor = ThreadPoolExecutor(max_workers=2)
 
future1 = executor.submit(printHello, 5, "Alice")
future2 = executor.submit(printHi, 5, "Bob")

future1.result()
future2.result()
        
print("すべての作業完了")

submitのパラメーターには単純に関数、そして関数内部に入れるパラメーターを順に入れるだけです。

submitの時点で関数の実行は開始され、resultを呼び出すと関数が終了するまで待機して戻り値を得ることができます。

resultを呼び出さないとメインスレッドが待たないため、プログラムが先に終了することもあります。

デフォルトで内部にdaemonが宣言されているため、別途宣言する必要はありません。

threadingよりも少し直感的です。

メモリリーク防止のため、以下のようにwith文で囲むこともできます。

with ThreadPoolExecutor(max_workers=2) as executor:
    future1 = executor.submit(printHello, 5, "Alice")
    future2 = executor.submit(printHi, 5, "Bob")
    
    future1.result()
    future2.result()

with文はexecutor.shutdown(wait=True)を自動的に呼び出し、安全にスレッドを終了します。

4. GUIと関数の同時実行例

では簡単なGUIと関数を同時に実行してみましょう。

GUIはcustomtkinterを使用します。

関数は上で行ったのと同様にprintHello, printHiを利用して実装しようと思います。

以下のようにクラスを実装しました。

from concurrent.futures import ThreadPoolExecutor
import time
import customtkinter as ctk

class helloHiWindow:
    def __init__(self):
        self.hello = 5
        self.hi = 6
        self.app = ctk.CTk()
        self.executor = ThreadPoolExecutor(max_workers=2)
        self.customWindow()
        
    def run(self):    
        self.app.mainloop()
        
    def customWindow(self):
        self.app.title("Hello! Hi!")
        self.app.geometry("300x200")
        button1 = ctk.CTkButton(self.app, text="Hello!", height=10, command=self.startHello)
        button1.pack(pady=20)
        button2 = ctk.CTkButton(self.app, text="Hi!", height=10, command=self.startHi)
        button2.pack(pady=20)
        
    def startHello(self):
        hello = self.executor.submit(self.printHello)
        
    def startHi(self):
        hi = self.executor.submit(self.printHi)
                
    def printHello(self):
        for i in range(self.hello):
            print(f'hello-{i}')
            time.sleep(1)
            
    def printHi(self):
        for i in range(self.hi):
            print(f'hi-{i}')
            time.sleep(1)
            
            
if __name__ == "__main__":
    app = helloHiWindow()
    app.run()

もしMacBookを使用しているなら、brew install python-tkでライブラリを1つインストールする必要があります。

こうして作成した後、ウィンドウを実行すると以下のようにウィンドウが表示されます。

ボタンを1つずつクリックすると出力が表示されます。

Pythonでマルチスレッド(並列実行)を実装する-5

ボタンをたくさんクリックしても、元のウィンドウは止まりません。

メインスレッドでウィンドウが動いていて、print関数はサブスレッドで別途実行しているからです。

このような方式でウィンドウは止まらずに、関数は実行されるようにできます。

5. これで実装したのが本当のマルチスレッドじゃないって...?

PythonにはGIL(Global Interpreter Lock)というものがあり、同時に複数のスレッドがCPUで実行することができません。

これはPythonの速度向上、開発安定性確保のために作られたとされています。

しかし、上のコードは実行してみると同時に実行されているように見えます。

Pythonでマルチスレッド(並列実行)を実装する-6

これはPythonコードが1つのスレッドで高速に交互に実行されているからです。

まるでJavaScriptのイベントループのようです。

time.sleep(1)のような関数はCPUを占有せず待機し、その間に他の必要な関数を処理します。

このような方式で実際には1つのスレッドですが、複数のスレッドが存在するように見せています。

6. 考察

初めてPythonを学んだとき、マルチスレッド(マルチタスキング)という概念がやや馴染みがなく難しく感じられました。

そのため単純にターミナルウィンドウだけ利用していましたが、開発段階に入るとターミナルの黒いウィンドウが非常に難しく感じられ、初めてcustomtkinterでGUIを実装してみました。

そしてそのように作成する過程で、マルチスレッドについて確実に理解しました。

プログラミングは講演や授業を聞くよりも、プロジェクトを1つ解決していく中で得るものが多いです。

今後も継続的にプロジェクトを進めてみようと思います。

댓글을 불러오는 중...