Edited at

Pythonでの並行処理と並列処理

More than 1 year has passed since last update.

今回は自分なりに並行処理と並列処理を処理の方法の違いから使い分けをしてみたいと思います.

プログラミング歴もQiitaへの投稿も経験が浅いので間違いや感想などあれば送っていただけると非常に励みになります.


環境


  • Ubuntu17.04

  • Python3.6.1

  • Intel core-i7 7500U


並行処理と並列処理の違い(基礎)

https://docs.python.org/3/library/concurrency.html

には並行処理を行うモジュールについて詳しく書いてあります.

並行処理(multi processing)と並列処理(multi threading)の違いというと,並行処理が今あるプロセスと別のプロセスを新たに起動し処理を行わせるのに対して,並列処理は今あるプロセスの中で複数のスレッドを立ち上げます.

料理でイメージすると,並行処理の立ち上げがそもそもの料理をする人数を増やすやり方で,並列処理の立ち上げが,料理をする人数は一人だが手が空いているときには他の仕事をやろうというやり方です.


並行処理と並列処理の違い(実践)

それではそれぞれについて実際に簡単な実装を行い,どのような違いがあるのか見てみましょう.

Linuxの場合はシステムモニターを起動してリソースから各CPUの使用率を確認しながらだとわかりやすいと思います.(コア数が一つしかないPCだと,おそらく今回のプログラムでは違いがわかりません)

まずは単純な加算処理ではどうでしょうか?


Multi Process


process_addition.py


import os
import multiprocessing

MAX_COUNT = 100000000
ITERATION = 50000000

def whoami(what):
#単純な加算
count = 0
for n in range(MAX_COUNT):
if count % ITERATION ==0:
#実行中のプロセスIDと,現在のcount数を表示
print("{} Process {} count {}".format(what,os.getpid(),count))
count +=1
#どのIDのプロセスが終了したかを表示
print("end {} Process {}".format(what,os.getpid()))
#現在のプロセスのidを表示
print("Main Process ID is {}".format(os.getpid()))
#メインのプロセスで実行
whoami("main program")

print("-----------------------------------------------------")
#プロセスを10作りスタートさせる.
for n in range(10):
p = multiprocessing.Process(target=whoami,args=("Process {}".format(n),))
p.start()
print("end of program")



Multi Thread


thread_addition.py

import os

import threading

MAX_COUNT = 100000000
ITERATION = 50000000

def whoami(what):
#単純な加算
count = 0
for n in range(MAX_COUNT):
if count % ITERATION ==0:
#実行中のプロセスIDと,現在のcount数とスレッドナンバーを表示
print("{} Process {} thread= {} count {}".format(what,os.getpid(),threading.current_thread(),count))
count +=1
#どのスレッドが終了したかを表示
print("end {} Thread {}".format(what,threading.current_thread()))
#現在のプロセスのidを表示
print("Main Process ID is {}".format(os.getpid()))
#メインのプロセスで実行
whoami("main program")

for n in range(10):
p = threading.Thread(target=whoami,args=("Thread {}".format(n),))
p.start()
print("end of program")



考察

この2つのプログラム(process_addition.pythread_addition.py)をそれぞれ実行してみてください.

実はこの2つのプログラム,実行する環境によって挙動に違いがあります.

jupyter notebook上で実行したときが最もわかりやすかったので,その結果から見てみます.


CPU使用率について

jupyter notebook上で実行するとそれぞれcpuの使用率は以下のようになります.



  • process_addition.py


    • cpu1:100%

    • cpu2:100%

    • cpu3:100%

    • cpu4:100%




  • thread_addition.py


    • cpu1:100%

    • cpu2:5%

    • cpu3:8%

    • cpu4:7%



このように並行処理(multi process)が各cpuでそれぞれプロセスを実行するのに対し,並列処理(multi thread)では一つのcpuで仕事をしています.

この部分から『並行処理は複数人での作業,並列処理はあくまで一人で作業』というイメージがつきやすいのではないでしょうか.


実行順序について

どちらのプログラムも最後は

print("end of program")

で終わっています.それではそれぞれを実行したとき,この文はそれぞれどこに出てきたでしょうか?

私の場合こんなふうになりました.


並列処理



Main Process ID is 17357
main program Process 17357 count 0
main program Process 17357 count 50000000
end main program Process 17357
-----------------------------------------------------
Process 0 Process 17363 count 0
Process 1 Process 17364 count 0
Process 2 Process 17365 count 0
end of program
Process 6 Process 17369 count 0
Process 3 Process 17366 count 0
Process 5 Process 17368 count 0
Process 7 Process 17370 count 0
Process 4 Process 17367 count 0
Process 8 Process 17371 count 0
Process 9 Process 17372 count 0
Process 3 Process 17366 count 50000000
Process 5 Process 17368 count 50000000

[...]


並行処理

Main Process ID is 17503

main program Process 17503 thread= <_MainThread(MainThread, started 140175289968384)> count 0
main program Process 17503 thread= <_MainThread(MainThread, started 140175289968384)> count 50000000
end main program Thread <_MainThread(MainThread, started 140175289968384)>
Thread 0 Process 17503 thread= <Thread(Thread-1, started 140175264700160)> count 0
Thread 1 Process 17503 thread= <Thread(Thread-2, started 140175183181568)> count 0
Thread 2 Process 17503 thread= <Thread(Thread-3, started 140175174788864)> count 0
Thread 3 Process 17503 thread= <Thread(Thread-4, started 140175166396160)> count 0
Thread 4 Process 17503 thread= <Thread(Thread-5, started 140175158003456)> count 0
Thread 5 Process 17503 thread= <Thread(Thread-6, started 140175149610752)> count 0
Thread 6 Process 17503 thread= <Thread(Thread-7, started 140175141218048)> count 0
Thread 7 Process 17503 thread= <Thread(Thread-8, started 140175132825344)> count 0
Thread 8 Process 17503 thread= <Thread(Thread-9, started 140174646310656)> count 0
Thread 9 Process 17503 thread= <Thread(Thread-10, started 140174637917952)> count 0
end of program
Thread 5 Process 17503 thread= <Thread(Thread-6, started 140175149610752)> count 50000000
Thread 7 Process 17503 thread= <Thread(Thread-8, started 140175132825344)> count 50000000
[...]

並列処理の場合新たにプロセスを立ち上げた場合,自分のプロセスと別のプロセスが立ち上がり,そのプロセスを別のcpuで実行します.

そのため,以下のようにプロセスが割り振られたのではないでしょうか.


  • cpu1→main_process→『end of program』→Process3...

  • cpu2→Process0→Process4...

  • cpu3→Process1→Process5...

  • cpu4→Process2→Process6...

並列処理の方はどうでしょう?

並列処理の方は現在のプロセスでスレッドを作っていくため,すべてのスレッドがスタートしてからend of programが出現しています.並行処理と比較するならこんな感じだと思います.


  • cpu1→Thread1→Thread2→....→Thread10→『end of program』→....

  • cpu2

  • cpu3

  • cpu4


端末上

上に載せたプログラムは,実行する環境によって違いがあると書きました.Jupyter Notebookで実行したときは上のような結果になったのですが,プログラムとして保存して実行するとthread_addition.pyの方のcpuの使用率が以下のようになりました.


  • cpu1:20〜30%

  • cpu2:20〜30%

  • cpu3:20〜30%

  • cpu4:20〜30%

どうもpythonで一つのプロセスを処理する際,単純に一つのcpuでやるのではなく複数のcpuで交代で処理をするようなのです.

このままだと実際どのようになっているかわかりにくいとおもいます.

これについて@gekichinさんからコメントで教えて頂いたjoin()メソッドを追加してみたいと思います.


join()を使った際の挙動の変化

join()メソッドを使うことで各処理が終わるまで,次の処理を開始することをなくしてくれます.

join()start()の直後に追加してもう一度端末で実行してみましょう.


thread_addition.py


[...]

for n in range(10):
p = threading.Thread(target=whoami,args=("Thread {}".format(n),))
p.start()
p.join()
print("end of program")




python3 thread_addition.py
Main Process ID is 12277
main program Process 12277 thread= <_MainThread(MainThread, started 140484185274112)> count 0
main program Process 12277 thread= <_MainThread(MainThread, started 140484185274112)> count 50000000
end main program Thread <_MainThread(MainThread, started 140484185274112)>
Thread 0 Process 12277 thread= <Thread(Thread-1, started 140484160005888)> count 0
Thread 0 Process 12277 thread= <Thread(Thread-1, started 140484160005888)> count 50000000
end Thread 0 Thread <Thread(Thread-1, started 140484160005888)>
Thread 1 Process 12277 thread= <Thread(Thread-2, started 140484149421824)> count 0

[...]

Thread 8 Process 12277 thread= <Thread(Thread-9, started 140484149421824)> count 50000000
end Thread 8 Thread <Thread(Thread-9, started 140484149421824)>
Thread 9 Process 12277 thread= <Thread(Thread-10, started 140484149421824)> count 0
Thread 9 Process 12277 thread= <Thread(Thread-10, started 140484149421824)> count 50000000
end Thread 9 Thread <Thread(Thread-10, started 140484149421824)>
end of program

各スレッドが順々に実行されているのがわかると思います.

そして,cpu使用率の方はどうでしょうか.

私の場合,スレッドが切り替わるたびに使用率100%になるcpuがランダムに切り替わっていました.

ここから先程のcpu使用率について推測してみます.

大量にスレッドを立てた場合,それらのスレッドは使うcpuを入れ替わり立ち代わり変えながら実行される.

しかし,処理はあくまで一つのプロセス内で逐次的なものなのです.

100%で実行されるcpuが高速で入れ替わっていたために,記録としておよそ4分の1の値20〜30%という値が残ったのでしょう.

(あくまで結果からみた推察です.間違っている,補足,参考資料,など知っている方がいたら教えてください...)


まとめ

今回の実験から,並行処理は以下のような場合に有効であると感じます.


  • cpuのリソースに余裕がある(複数のコアがあり,すべてのコアが十分に動いていない)

  • メインのプロセスと別で処理をする利点がある.


私の使用例

webサイトの会員登録ページ


問題点


  • 登録をemailを介して行いたい

  • 登録ボタンクリック→email送信→リダイレクト→ページ表示はメール送信の部分で大きな時間損失がある(ページの表示速度はユーザのサイト離脱率に大きく関係).


並行処理を使って解決

1.登録ボタンをクリック

2.email送信を他のプロセスに丸投げ

3.メインプロセスの方ではそのままページを表示

この方法でメール送信までのタイムラグの影響をなくすことができます.