Pythonのthreadとprocessの違いについて改めて確認してみる。
- 2021.08.14
- IT
目次
はじめに
適当に使っていたスレッドとプロセスって何が違うのだろう、と思い、コードの挙動を見ながら検証してみました。
検証環境
Windows10 Home
Python3.9.2
用語の定義
- マルチタスク
- 複数のプログラムコードを同時並行に実行させる仕組み
- マルチタスクシステム
- 何をタスクにするのかによってさまざまだが、「プロセス」を単位とする「マルチプロセス」と「スレッド」を単位とする「マルチスレッド」がよく用いられる。
- プロセス
- (おおよそ)アプリケーションに対応し、他のプロセスと動作ができるだけ干渉しないようにメモリ空間、ファイルハンドルなどの計算機資源が独立して管理されている
- プロセス間の関係は「粗」
- 他のプロセスのメモリには(原則)直接アクセスできない
- スレッド
- 1つのプロセス内で並列処理を行うための機構。多くの計算機資源を他のスレッドと共有する。
- OSの視点だと、スレッドがプログラムの最少単位であり、「計算機資源を共有するいくつかのスレッドをまとめて取り扱う単位がプロセスである」ということができる。
- スレッド間の関係は「密」
- 他のスレッドのメモリに直接アクセスできる(メモリ空間が同一)
- 指定しない場合、処理の順番はOSの気まぐれ
Threadとprocessの違い
主な違い
下記の表のような違いがありますが、グローバル変数を共有できる(スレッド)か、できない(プロセス)か、というのは利用上大きな違いがあるかと思います。
計算機資源 | プロセス | スレッド |
メモリ | 他のプロセスのメモリには(原則として)直接アクセスできない | 他のスレッドのメモリに直接アクセスできる。(メモリ空間が同一) |
ファイルハンドル | 他のプロセスが作成したファイルハンドルを操作することはできない。 | 他のスレッドが作成したファイルハンドルを使うことができる。 |
ファイル実体 | ファイルパス名が同じファイルは(原則として)すべてのプロセスで同じもの。 | ファイルパス名が同じファイルは(原則として)すべてのスレッドで同じもの。 |
スレッド | 他のプロセス内にスレッドを作成したり他のプロセス内のスレッドを停止したりすることは(原則として)できない。 | 他のスレッドが作成したスレッドを操作することができる。 |
Linuxとpthreadによる マルチスレッドプログラミング入門P2より
メモリ空間のイメージ
下記の図のように、スレッドでは、同一のメモリを共用していますが、
プロセスはメモリを各プロセスのメモリへコピーをしています。
この仕組みの違いが、スレッドとプロセスの挙動の違いを生み出しています。
コードで違いを見てみる
Process
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
from multiprocessing import Process import os, signal import time import multiprocessing as mp global_var = 0 print("global var id is ", id(global_var)) def run_process(name): print('Run child process %s (%s)...' % (name, os.getpid())) global global_var print("children's global var id is ", id(global_var)) print("children's global var val is ", global_var) time.sleep(5) global_var = os.getpid() print("children's global var val is ", global_var) # メイン関数 def main(): print('Parent process %s.' % os.getpid()) parent_pid = os.getpid() global global_var print("parent's var id is ", id(global_var)) print("parent's var val is ", global_var) global_var = 456 p = Process(target=run_process, args=('test',)) print(p) print('start child process.') p.start() time.sleep(5) global_var = 789 print(p) p.join() print(p) print("parent after child val is ", global_var) print("parent after child id is ", id(global_var)) print('end child process.') # メインプログラム if __name__=='__main__': global_var = 123 #ここでの代入は子プロセスには反映されない.多分上のグローバル変数で既に反映させられている。 print("main process var var is ", global_var) print("main process var id is ", id(global_var)) mp.freeze_support() # ProcessをWindowsで使うにはこれが必要 main() |
上記の実行結果はこうなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
""" global var id is 2053876508944 main process var var is 123 main process var id is 2053876701360 Parent process 11304. parent's var id is 2053876701360 parent's var val is 123 <Process name='Process-1' parent=11304 initial> start child process. <Process name='Process-1' pid=12320 parent=11304 started> global var id is 2827925416208 Run child process test (12320)... children's global var id is 2827925416208 children's global var val is 0 children's global var val is 12320 <Process name='Process-1' pid=12320 parent=11304 stopped exitcode=0> parent after child val is 789 parent after child id is 2053882547088 end child process. Process finished with exit code 0 """ |
注目していただきたいのは、11行目です。
プロセスが起動された際に、グローバル変数がまた初期化されています。
つまり、変数名が同じグローバル変数が、メモリの別の場所に宣言されている、ということです。
これはメモリの場所(id)の値を見ても分かりますね。
実装する際の注意点としては、親プロセスでグローバル変数の値を変更していたとしても、
子プロセスのグローバル変数の値は、グローバル変数が宣言時の値になる、ということです。(14行目)
Thread
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
import threading import os import time import multiprocessing as mp global_var = 0 print("global var id is ", id(global_var)) def run_thread(name): print('Run child process %s (%s)...' % (name, os.getpid())) global global_var print("children's global var id is ", id(global_var)) print("children's global var val is ", global_var) time.sleep(5) global_var = os.getpid() print("children's global var val is ", global_var) print("children's global var id is ", id(global_var)) # メイン関数 def main(): print('Parent process %s.' % os.getpid()) parent_pid = os.getpid() global global_var print("parent's global var id is ", id(global_var)) print("parent's global var val is ", global_var) global_var = 456 p = threading.Thread(target=run_thread, args=('test',)) print(p) print('start child process.') p.start() time.sleep(5) global_var = 789 print(p) p.join() print(p) print("parent after child global var val is ", global_var) print("parent after child global_var id is ", id(global_var)) print('end child process.') # メインプログラム if __name__=='__main__': global_var = 123 #ここでの代入は子プロセスには反映されない.多分上のグローバル変数で既に反映させられている。 print("main process global var val is ", global_var) print("main process global var id is ", id(global_var)) mp.freeze_support() # ProcessをWindowsで使うにはこれが必要 main() |
上記の実行結果は下記になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
""" global var id is 2862526392592 main process global var val is 123 main process global var id is 2862526585008 Parent process 6116. parent's global var id is 2862526585008 parent's global var val is 123 <Thread(Thread-1, initial)> start child process. Run child process test (6116)... children's global var id is 2862527514768 children's global var val is 456 children's global var val is <Thread(Thread-1, started 19580)> 6116 children's global var id is 2862527515536 <Thread(Thread-1, stopped 19580)> parent after child global var val is 789 parent after child global_var id is 2862527515536 end child process. Process finished with exit code 0 """ |
メモリを共有しているので、グローバル変数の値は親子で相互に影響を受けています。
まとめ
大きな違いは、メモリを共有しているか、していないか、だと私は感じました。
そこから各手法のメリットデメリットが生じてくると思います。
参考文献
Linuxとpthreadによる マルチスレッドプログラミング入門