【Python 雑談・雑学】 Python におけるマルチスレッド実行 (multi threading) について - マルチスレッドで実行すれば何でもかんでも速くなる、と思っていませんか? - 投稿一覧へ戻る

Tags: Python , miscellaneous , process.thread , gil

Published 2020年6月1日8:26 by T.Tsuyoshi

まずは確認から。


プロセスは、1つ以上のスレッドとその実行に必要なリソース群 (CPU cores, network, file pointers etc...)、をひとまとめにしているものです。


Python ではこのリソース群のことを GIL (Global Interpreter Lock) と呼びます。


1つのプロセスは複数のスレッドを保持することができますが、GIL はたった1つしか持つことができません。


Python ではスレッドが実行される際、そのスレッドが GIL を所有しているかがチェックされます。


よって、1つのプロセスで1度に実行できるスレッドは常に1つだけです
(たとえそのプロセスが2つのスレッドを持ち、2つのコアにそれぞれのスレッドを配置していたとしても、です)。


また CPU の1つのコアで1度に実行できるスレッドも1つだけです。


ですから、どんなにコア数が多い CPU を使用していても、プロセスが1つだけしか動いていない状況下では、動作しているコアは1つだけ、ということになります(あくまでも Python のプログラムに関してです)。


すなわち、Python におけるマルチスレッドというのは、GIL を保有しているスレッドが GIL を手放し、それを受け取った他のスレッドが実行され、そのスレッドがまた GIL を手放して...


といったように、GIL の受け渡しによって制御、実現されているものなんです
(これによって、よく言われる Dining philosophers problem - 食事する哲学者の問題 - を解決しています)。


じゃあ、マルチスレッドなんて意味ないじゃん。


そうなんです、意味を持つときもありますが、意味がないどころか逆効果になることさえあるんです。


では、Python においてマルチスレッドが意味を成すのはどんな時なのでしょうか?


それはずばり、ある1つのスレッドにおいて (CPU の動作速度からすれば) 莫大な待ち時間が発生する場合です。


例えば、ユーザーからの入力を受け付ける、ファイルにアクセスする、等を含む処理を実行する場合です。


このような場合、その時間のかかる操作を待つ間に GIL を一旦開放し、他のスレッドを動作させ、時間がかかる操作が終了した時点で GIL を返して処理を続けることで、シングルスレッド実行では延々と待っていなければならない時間を有効活用できます。


はっきり言って、Python におけるスレッド活用法は、「待ち時間を減らす」、これに尽きます。


逆に言えば、待ち時間がほぼ発生しないような複雑な計算処理を実行するだけの場合、いくら複数のスレッドに分割しても GIL の受け渡し等に関わるオーバーヘッドが生じる分、かえって全体の処理速度が落ちてしまうんです。


と、文字ばかり羅列してもなかなか伝わりづらいと思いますから、実際に動かして確認してみましょう。


import time
from threading import Thread

# 待ち時間が発生する処理
def ask_user():
start = time.time()
user_input = input("名前を入力してください : ")
greet = f"Hello, {user_input}!"
print(greet)
print(f"ask_user, {time.time() - start}")

# 待ち時間はほぼ発生しない処理
def complex_calculation():
start = time.time()
print("Start calculating ...")
[x**2 for x in range(30000000)]
print(f"complex_calculation, {time.time() - start}")



上の2つの処理をまずシングルスレッドで実行してみましょう。


start = time.time()
ask_user()
complex_calculation()
print(f"Single thread total time, {time.time() - start}\n")

Hello, Nana!
ask_user, 1.89070725440979
Start calculating ...
complex_calculation, 11.590662956237793
Single thread total time, 13.482370376586914




続けて複数のスレッドで動作させます。


thread1 = Thread(target=complex_calculation)
thread2 = Thread(target=ask_user)

start = time.time()

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(f"Two threads total time, {time.time() - start}")

Start calculating ...
Hello, Nana!
ask_user, 1.8741071224212646
complex_calculation, 11.639665842056274
Two threads total time, 11.641665697097778




total time と complex_calculation の実行時間がほぼ変わらないことに注目です。
すなわち、ask_user の実行時間のほとんど全てはユーザーからの入力待ち時間で、
このような状況の場合は複数スレッドにおける実行は非常に有効であることが分かると思います。


では、2つのスレッドとも複雑な計算をしなければいけない場合を見てみましょう。


まずはシングルスレッドで。


start = time.time()
complex_calculation()
complex_calculation()
print(f"Single thread total time, {time.time() - start}\n")

Start calculating ...
complex_calculation, 11.879278659820557
Start calculating ...
complex_calculation, 11.791674613952637
Single thread total time, 23.671953201293945




つづいて複数のスレッドで。


thread1 = Thread(target=complex_calculation)
thread2 = Thread(target=complex_calculation)

start = time.time()

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(f"Two threads total time, {time.time() - start}")

Start calculating ...
Start calculating ...
complex_calculation, 24.083377838134766
complex_calculation, 24.088377952575684
Two threads total time, 24.089377880096436




複数スレッドでの実行の方が合計処理時間が増加しています。
これは GIL の受け渡し等によるオーバーヘッドが生じている反面、待ち時間がほぼないため逆効果になってしまっているからです。


このように Python におけるマルチスレッドの利用は、待ち時間を有効活用するためのもの、と考えて間違いないと思います。

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)