『Pythonでのマルチスレッディングとマルチプロセッシングの紹介』
『美とファッションの世界への魅惑的な案内』
このチュートリアルでは、Pythonのマルチスレッティングとマルチプログラミングのタスクを実行する能力を活用する方法について説明します。これらは、単一のプロセス内または複数のプロセス間で並行操作を実行するためのゲートウェイを提供します。並行して実行されることにより、システムの速度と効率が向上します。マルチスレッティングとマルチプログラミングの基礎について説明した後、Pythonライブラリを使用した実際の実装についても説明します。まず、並列システムの利点について簡単に説明しましょう。
- パフォーマンスの向上:タスクを並行して実行できるため、実行時間を短縮し、システム全体のパフォーマンスを向上させることができます。
- スケーラビリティ:大きなタスクをさまざまな小さなサブタスクに分割し、それぞれに独自のコアまたはスレッドを割り当てて独立した実行を行うことができます。大規模なシステムに役立ちます。
- I/O操作の効率化:並行処理により、CPUはプロセスがI/O操作を完了するのを待つ必要がありません。CPUは前のプロセスがそのI/O作業に忙しい間、すぐに次のプロセスの実行を開始することができます。
- リソースの最適化:リソースを分割することで、単一のプロセスがすべてのリソースを占有することを防ぐことができます。これにより、小規模なプロセスに対するStarvationの問題を回避することができます。
これらは、並行または並列実行が必要な一般的な理由です。今度はメインのトピックであるマルチスレッティングとマルチプログラミングに戻り、それらの主な違いについて説明します。
マルチスレッティングとは何ですか?
マルチスレッティングは、単一のプロセス内で並列処理を実現する方法の一つであり、同時にタスクを実行することができます。複数のスレッドを単一のプロセス内に作成し、そのプロセス内で並行して小さなタスクを実行することができます。
単一のプロセス内のスレッドは共有のメモリスペースを共有していますが、スタックトレースとレジスタは別々です。この共有メモリにより、計算コストが低くなります。
マルチスレッティングは主にI/O操作を実行するために使用されます。つまり、プログラムの一部がI/O操作に忙しい場合、残りのプログラムは応答性があります。ただし、Pythonの実装では、グローバルインタプリタロック(GIL)のため、マルチスレッティングは真の並列実行を達成することはできません。
要するに、GILはミューテックスロックであり、一度に1つのスレッドがPythonバイトコードとやり取りできるようにします。つまり、マルチスレッティングモードでも、一度に1つのスレッドしかバイトコードを実行できません。
これは、CPythonでスレッドセーフを維持するために行われていますが、これによりマルチスレッティングのパフォーマンスの利点が制限されます。この問題を解決するために、Pythonには独自のマルチプロセッシングライブラリがあり、それについては後ほど説明します。
デーモンスレッドとは何ですか?
バックグラウンドで常に実行されるスレッドはデーモンスレッドと呼ばれます。彼らの主な仕事は、メインスレッドまたは非デーモンスレッドのサポートです。デーモンスレッドは、実行が完了してもメインスレッドの実行をブロックせず、実行し続けます。
Pythonでは、デーモンスレッドは主にガベージコレクタとして使用されます。デーモンスレッドは不要なオブジェクトをすべて破棄し、メモリをデフォルトで解放するため、メインスレッドが正しく使用および実行されることができます。
マルチプロセッシングとは何ですか?
マルチプロセッシングは複数のプロセスの並列実行を行うために使用されます。独自のメモリスペースを持つ別々のプロセスを同時に実行することができます。CPUの別々のコアを使用し、複数のプロセス間でデータを交換するためのプロセス間通信を実行するのにも役立ちます。
マルチプロセッシングは、共有メモリスペースを使用しないため、マルチスレッティングと比較してより計算コストが高くなりますが、独立した実行とグローバルインタプリタロックの制限を克服することができます。
上記の図は、メインプロセスが2つの別々のプロセスを作成し、それぞれに別々の作業を割り当てるマルチプロセッシング環境を示しています。
マルチスレッドの実装
Pythonを使って基本的なマルチスレッドの例を実装しましょう。Pythonには、マルチスレッドの実装に使用する組み込みのthreading
モジュールがあります。
- ライブラリのインポート:
import threadingimport os
- 平方を計算する関数:
これは、数値の平方を求めるための単純な関数です。入力として数値のリストを受け取り、リスト内の各数値の平方と、そのスレッドの名前、およびそのスレッドに関連するプロセスIDを出力します。
def calculate_squares(numbers): for num in numbers: square = num * num print( f"数値{num}の平方は{square} | スレッド名 {threading.current_thread().name} | プロセスID {os.getpid()}" )
- メイン関数:
数値のリストがあります。このリストを等分割し、それぞれを「fisrt_half」と「second_half」として名前を付けます。そして、これらのリストにそれぞれ別々のスレッドt1
とt2
を割り当てます。
Thread
関数は、リスト型の引数を受け取る関数を持つ新しいスレッドを作成します。スレッドには別々の名前を割り当てることもできます。
.start()
関数はこれらのスレッドの実行を開始し、.join()
関数は指定したスレッドが完全に実行されるまで、メインスレッドの実行をブロックします。
if __name__ == "__main__": numbers = [1, 2, 3, 4, 5, 6, 7, 8] half = len(numbers) // 2 first_half = numbers[:half] second_half = numbers[half:] t1 = threading.Thread(target=calculate_squares, name="t1", args=(first_half,)) t2 = threading.Thread(target=calculate_squares, name="t2", args=(second_half,)) t1.start() t2.start() t1.join() t2.join()
出力:
数値1の平方は1 | スレッド名 t1 | プロセスID 345数値2の平方は4 | スレッド名 t1 | プロセスID 345数値5の平方は25 | スレッド名 t2 | プロセスID 345数値3の平方は9 | スレッド名 t1 | プロセスID 345数値6の平方は36 | スレッド名 t2 | プロセスID 345数値4の平方は16 | スレッド名 t1 | プロセスID 345数値7の平方は49 | スレッド名 t2 | プロセスID 345数値8の平方は64 | スレッド名 t2 | プロセスID 345
注意: 上記で作成したすべてのスレッドは非デーモンスレッドです。デーモンスレッドを作成するには、スレッド
t1
をデーモンスレッドにするためにt1.setDaemon(True)
と記述する必要があります。
さて、上記のコードによって生成される出力を理解しましょう。プロセスID(つまりPID)が両方のスレッドで同じであることがわかります。つまり、これらの2つのスレッドは同じプロセスの一部であることを意味します。
また、出力が順次生成されないことも確認できます。最初の行では、thread1によって生成された出力が表示され、3行目ではthread2によって生成された出力、そして4行目では再びthread1によって生成された出力が表示されます。これは、これらのスレッドが同時に協調して動作していることを明示的に示しています。
並行性は、これらの2つのスレッドが並列に実行されることを意味するわけではありません。一度に1つのスレッドしか実行されないため、実行時間は短縮されません。CPUはスレッドの実行を開始しますが、途中で中断して別のスレッドに移動し、しばらくしてからメインスレッドに戻り、前回停止した地点から実行を再開します。
マルチプロセス実装
マルチスレッディングとその実装および制約についての基本的な理解をお持ちいただけることを望みます。さて、それらの制約を克服するためのマルチプロセス実装について学びましょう。
同じ例を使いますが、2つの別々のスレッドではなく、2つの独立したプロセスを作成し、観測結果について論じます。
- ライブラリのインポート:
from multiprocessing import Processimport os
独立したプロセスを作成するためにmultiprocessing
モジュールを使用します。
- 平方を計算する関数:
その関数は同じままです。スレッド情報のプリントステートメントを削除しただけです。
def calculate_squares(numbers): for num in numbers: square = num * num print( f"数値 {num} の二乗は {square} です。プロセスのPIDは {os.getpid()}です。" )
- メイン関数:
メイン関数にいくつかの変更があります。スレッドの代わりに独立したプロセスを作成しました。
if __name__ == "__main__": numbers = [1, 2, 3, 4, 5, 6, 7, 8] half = len(numbers) // 2 first_half = numbers[:half] second_half = numbers[half:] p1 = Process(target=calculate_squares, args=(first_half,)) p2 = Process(target=calculate_squares, args=(second_half,)) p1.start() p2.start() p1.join() p2.join()
出力:
数値 1 の二乗は 1 です。プロセスのPIDは 1125です。数値 2 の二乗は 4 です。プロセスのPIDは 1125です。数値 3 の二乗は 9 です。プロセスのPIDは 1125です。数値 4 の二乗は 16 です。プロセスのPIDは 1125です。数値 5 の二乗は 25 です。プロセスのPIDは 1126です。数値 6 の二乗は 36 です。プロセスのPIDは 1126です。数値 7 の二乗は 49 です。プロセスのPIDは 1126です。数値 8 の二乗は 64 です。プロセスのPIDは 1126です。
それぞれのリストを別々のプロセスが実行していることが観察されました。両者は異なるプロセスIDを持っています。プロセスが並列に実行されているかどうかを確認するには、別々の環境を作成する必要がありますが、それについては以下で議論します。
マルチプロセスを使用した実行時間の計算
真の並列性を得るかどうかを確認するために、マルチプロセスとそのなしでアルゴリズムの実行時間を計算します。
これには10^6を超える整数を含む大規模な整数のリストが必要です。リストを作成するにはrandom
ライブラリを使用します。ランタイムを計算するためにPythonのtime
モジュールを使用します。以下にその実装を示します。コードは自明ですが、いつでもコードのコメントを参照することもできます。
from multiprocessing import Processimport osimport timeimport randomdef calculate_squares(numbers): for num in numbers: square = num * numif __name__ == "__main__": numbers = [ random.randrange(1, 50, 1) for i in range(10000000) ] # サイズが10^7の整数のランダムなリストを作成しています。 half = len(numbers) // 2 first_half = numbers[:half] second_half = numbers[half:] # ----------------- シングルプロセス環境の作成 ------------------------# start_time = time.time() # マルチプロセスを使用しない場合の開始時間 p1 = Process( target=calculate_squares, args=(numbers,) ) # P1というシングルプロセスがすべてのリストを実行しています p1.start() p1.join() end_time = time.time() # マルチプロセスを使用しない場合の終了時間 print(f"マルチプロセスを使用しない実行時間: {(end_time-start_time)*10**3}ms") # ----------------- マルチプロセス環境の作成 ------------------------# start_time = time.time() # マルチプロセスを使用する場合の開始時間 p2 = Process(target=calculate_squares, args=(first_half,)) p3 = Process(target=calculate_squares, args=(second_half,)) p2.start() p3.start() p2.join() p3.join() end_time = time.time() # マルチプロセスを使用する場合の終了時間 print(f"マルチプロセスを使用した実行時間: {(end_time-start_time)*10**3}ms")
出力:
マルチプロセスを使用しない実行時間:619.8039054870605msマルチプロセスを使用した実行時間:321.70287895202637ms
マルチプロセスを使用した方が実行時間がマルチプロセスを使用しない方のほぼ半分であることがわかります。これにより、これらの2つのプロセスが同時に実行され、真の並列性の振る舞いを示していることがわかります。
また、VoAGIの「Sequential vs Concurrent vs Parallelism」という記事も読むことができます。これにより、この「Sequential」、「Concurrent」、および「Parallel」プロセスの基本的な違いを理解するのに役立ちます。
[Aryan Garg](https://www.linkedin.com/in/aryan-garg-1bbb791a3/)は、現在学部の最終年度、B.Tech.電気工学の学生です。彼の関心は、ウェブ開発と機械学習の分野にあります。彼はこの関心を追求し、これらの方向性でさらに積極的に取り組むことを熱望しています。
We will continue to update VoAGI; if you have any questions or suggestions, please contact us!
Was this article helpful?
93 out of 132 found this helpful
Related articles