今さらPython3 (72) - 並行処理 multiprocessing vs threading

ここから第11章に突入。Pythonの初心者でもないのに、この「入門」を買おうと思ったのは、この11章があったから。自分の全く知らない内容がカバーされているなら、勉強しても損はないかなと思ったわけで。

入門 Python 3

入門 Python 3

並行処理

個人的には「並列処理」のほうがなじみがあるんだけど、Pythonのドキュメントでは並行処理を謳っているので、こっちの用語で言った方が良さそう。

17. Concurrent Execution — Python 3.5.1 documentation
17. 並行実行 — Python 3.4.3 ドキュメント

あまり本に書いてあることをそのまま書くのもと思いつつ、大事そうな言葉なので整理しとく。

  • I/Oバウンド

インプット、アウトプットに縛られた(bound)処理。スタンドアロンならディスクへの読み書き、分散型ならネットワークなんかがボトルネックになるってこと。となると、並列数を増やしてもI/Oに待ちが発生するので、あまり効果がないって某所で習った気がする。

  • CPUバウンド

CPUがボトルネックとなる処理。これは使えるCPUが多ければ処理速度が改善するので、並列数を増やすことで対応する事が可能。マシン全体で使えるリソースは限られるので、もちろん限度はある。

分かっている人が読んだら怒られそうな解説だけど、とっかかりとしてはこんな理解で良いのでは?

  • 同期的

自分の知っている同期処理というのは、処理と同時に更新処理が走り、その更新が終わるまで待ってから処理を継続すること。葬式の行列のように順番に続いていくことという本の定義とだいたい一致するのかな?

  • 非同期的

自分の知っている非同期処理というのは、処理をする人(プロセス)と更新がする人が別れてていて、処理する人が更新内容を伝えると、更新する人が更新する。処理する人は結果を待たずして、次に着手するというイメージ。パーティーの参加者が別々の車で集まり、解散していくようにタスクが独立しているという説明に合致しているだろうか?

いずれにしても、処理したい内容を複数のタスクに分割して、上手く割り振る必要があるというのは理解できる。

キュー

とりあえずサンプルを見てみよう。動作の違いを見るために2つ用意してみた。

まず1つ目の教科書通りの実行結果。

$ python3 dishes.py
Washing salad dish
Washing bread dish
Washing entree dish
Washing dessert dish
Drying salad dish
Drying bread dish
Drying entree dish
Drying dessert dish

2つ目のsleepを挟んだ実行結果。

$ python3 dishes2_sleep.py
Washing salad dish
Drying salad dish
Washing bread dish
Drying bread dish
Washing entree dish
Drying entree dish
Washing dessert dish
Drying dessert dish

なんでわざわざ居眠りさせたのかというと、ソースを見た時に、dryer_proc.start()によって"乾燥担当プロセス"が先に起動しているように見えたから。ドキュメントを見ながら、理解し(ようとし)てみる。

17.2. multiprocessing — プロセスベースの並列処理 — Python 3.3.6 ドキュメント

dish_queueは、JoinableQueue()が割り当てられていて、意味としてはFIFO型のキューですという意味。dryer_procはプロセスを割り当てていて、dryer関数を引数dish_queueで実行するように指示している。daemonがTrueだと、これは子プロセスだよという宣言をしていて、start()でプロセスを開始している。

dishesのリストを作って、その後washer()関数が、dishesとdish_queueの引数を伴って実行されると。washer()関数の中では、dishesのリストの中をforでloopして、出力するのと同時に、dish_queue(関数の中ではoutput)にputする事でキューにエントリを追加している。最後にdish_queue.join()されていると言う事は、処理が終了されるまでキューがブロックされるんだね。

dryer()関数に戻って中身を確認。dish_queueからget()するというのは、キューから要素を取り出して削除するという意味。その後、print()したあとにtask_done()で、タスクが終了したことを宣言しているという流れみたいだね。

2つめのdishes2_sleep.pyでは、キューに追加する前に待ち時間を1秒おいているんだけど、dryer()はキューを常に見ている感じなので、次の処理がキューに追加されるまで1秒待っている間に処理を進めるので、結果的にwashingとdryingが交互に出る形になったと理解すれば良さそう。ちなみにキューのロックは、4個目のdishがキューに追加された後に掛かっているから、変な問題を引き起こさなかったんだね。

スレッド

スレッドの場合は、プロセス内で実行されるので、プロセス内のすべてのものにアクセスできるとか。
これも例をひもとく感じでやる。


これはthreads.pyの実行結果。

$ python3 threds.py
Thread <_MainThread(MainThread, started 140735299531520)> says: I'm the main program
Thread <Thread(Thread-1, started 4335693824)> says: I'm function 0
Thread <Thread(Thread-2, started 4335693824)> says: I'm function 1
Thread <Thread(Thread-3, started 4335693824)> says: I'm function 2
Thread <Thread(Thread-4, started 4335693824)> says: I'm function 3

4スレッド起動しているから、こんな感じだよね。引き続いてthreads_dishes.pyの結果。

$ python3 threads_dishes.py
Washing salad
Washing bread
Drying salad
Washing entree
Drying bread
Washing dessert
Drying entree
Drying dessert

こっちは、ソースコードを見るとwashingを出してキューに追加するまでに5秒まって、さらにDryingを出して10秒待ってからtask_done()を出している。Drying breadとWashing dessertは順序が逆になる可能性はあったかも知れないけど、まあ妥当な結果かと。

threadingにはterminate()がないとか、スレッドセーフでなくてはならないあたりの説明はごもっとも。スレッドの場合は、同じプロセスの中で処理するので、その中のモノであれば何でもアクセス可能になり、それを書き換えてしまうことによりデバッグしてもよく分からないような問題が発生する可能性があるということね。複数のスレッドで同じオブジェクトを同時に更新しないようにするためにロックを掛けるとか、気をつける点は多い。

ここらの概念も押さえておくべし。
スレッドセーフ - Wikipedia
グローバルインタプリタロック - Wikipedia

プロセスとスレッドの簡単な切り分けとしては、

  • スレッドはI/Oバウンド処理の解決のために使う。
  • CPUバウンド問題では、プロセス、ネットワーキング、イベントを使う。

これ先に言ってくれれば良かったのに。

(つづく)