検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
  • ただいまサイドメニューのテスト中/ただいまサイドメニューのテスト中
>>

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

Published 2020年6月1日8:26 by mootaro23

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

まずは確認から。


プロセスは、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 におけるマルチスレッドの利用は、待ち時間を有効活用するためのもの、と考えて間違いないと思います。
この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -
【 Using Asyncio in Python 】Python における asyncio を利用した非同期プログラム ( asynchronous programming ) の勉強を継続する前に、今一度スレッド ( thread ) についてのちょっとした復習ノート、まとめてみた
【 Effective Python, 2nd Edition 】threading モジュールの Lock クラスを利用してマルチスレッド実行時のデータ競合 ( data races ) を予防しよう! GIL はデータ構造 ( data structure ) の安全性まで面倒を見てくれません
【 Effective Python, 2nd Edition 】Python のスレッド ( thread ) はブロッキング I/O ( blocking I/O ) 対策で存在しています。決して並行処理 ( parallelism ) を実現するためではありません!
【 Effective Python, 2nd Edition 】Queue クラスを利用した producer-consumer パイプライン ( pipelines ) を構築して、マルチスレッドシーケンス処理をエレガントに管理しよう! 並行実行 ( parallelism ) と並列処理 ( concurrency ) もついでにちゃんとイメージしよう!
【Python 雑談・雑学】 ユーザー入力に応じて異なる処理関数を実行する際の少し面白い実装方法 - ユーザーの選択項目とそれに応じる関数名をディクショナリで保持する -
Python coding challenge - 和が target に等しい2つの要素をより速い実行時間で見つけよう!
【Python 雑談・雑学 + coding challenge】シーケンス ( sequence ) における インデックス ( index ) を使った要素 1 つの取り出しと、スライス ( slice ) を利用した場合の取り出しの違いをちゃんと理解していますか?