Pythonにおける並行処理について
気になったのでPythonのGIL(Global Interpreter Lock)が並行処理にどの程度影響するかについて少し実験しました。
はじめに
まず、「並行」処理と「並列」処理という言葉を区別する必要があります。下記参考文献の「並行コンピュータ技法」によると、
システムが複数の動作を同時に実行状態(in progress)に保てる機能を備えている場合を並行(concurrent)と言い、 複数の動作を同時に実行できる場合を並列(parallel)と言います。 重要な概念、違いは「実行状態」という点です。 ...中略... 「並行」は「並列」を含有します。
だそうです。同書によると、1つのCPUコアが2つのスレッドを切り替えながら処理する場合は「並行」処理に含まれるようです。「並列」処理では複数のCPUコアが必須で、複数のスレッドが複数のCPUコアにより同時に実行される事を「並列」処理と言うようです。
目指すべきは「並行」処理ではなく「並列」処理な気がします。
テストコード
1. 逐次処理でのCPU負荷の大きい処理
# sequential_cpu.py def _cpu_bound_work(): i = 0 while i < 100000000: i += 1 if __name__ == '__main__': for _ in xrange(8): _cpu_bound_work()
2. threadingでのCPU負荷の大きい処理
# threading_cpu.py import threading def _cpu_bound_work(): i = 0 while i < 100000000: i += 1 class TestThread(threading.Thread): def run(self): _cpu_bound_work() if __name__ == '__main__': mainthread = threading.currentThread() for _ in xrange(8): thread = TestThread() thread.start() for thread in threading.enumerate(): if mainthread != thread: thread.join()
3. multiprocessingによるCPU負荷の大きい処理
# multiprocessing_cpu.py import multiprocessing def _cpu_bound_work(): i = 0 while i < 100000000: i += 1 class TestProcess(multiprocessing.Process): def run(self): _cpu_bound_work() if __name__ == '__main__': for _ in xrange(8): process = TestProcess() process.start() for process in multiprocessing.active_children(): process.join()
4. 逐次処理によるIO待ちの大きい処理
# sequential_io.py import time def _io_bound_work(): time.sleep(10.0) # to simulate i/o bound work if __name__ == '__main__': for _ in xrange(8): _io_bound_work()
5. threadingによるIO待ちの大きい処理
# threading_io.py import threading import time def _io_bound_work(): time.sleep(10.0) # to simulate i/o bound work class TestThread(threading.Thread): def run(self): _io_bound_work() if __name__ == '__main__': mainthread = threading.currentThread() for _ in xrange(8): thread = TestThread() thread.start() for thread in threading.enumerate(): if mainthread != thread: thread.join()
6. multiprocessingによるIO待ちの大きい処理
# multiprocessing_io.py import multiprocessing import time def _io_bound_work(): time.sleep(10.0) # to simulate i/o bound work class TestProcess(multiprocessing.Process): def run(self): _io_bound_work() if __name__ == '__main__': for _ in xrange(8): process = TestProcess() process.start() for process in multiprocessing.active_children(): process.join()
テスト結果
1. 逐次処理によるCPU負荷の大きい処理
$ time python sequential_cpu.py real 0m45.265s user 0m45.230s sys 0m0.020s
2. threadingによるCPU負荷の大きい処理
$ time python threading_cpu.py real 1m8.033s user 1m7.420s sys 0m16.930s
3. multiprocessingによるCPU負荷の大きい処理
$ time python multiprocessing_cpu.py real 0m10.969s user 1m24.960s sys 0m0.040s
4. 逐次処理によるIO待ちの大きい処理
$ time python sequential_io.py real 1m20.095s user 0m0.010s sys 0m0.010s
5. threadingによるIO待ちの大きい処理
$ time python threading_io.py real 0m10.029s user 0m0.020s sys 0m0.000s
6. multiprocessingによるIO待ちの大きい処理
$ time python multiprocessing_io.py real 0m10.035s user 0m0.020s sys 0m0.010s
まとめ
1,4の逐次処理が遅いのは当然として、2のthreadingモジュールを使用してCPU負荷の大きい処理を行った場合の実行速度がかなり遅いです。
pythonのGILの影響で、並列処理ができていない事が原因なのでしょう。3のmultiprocessingモジュールを使用した場合はGILを回避できるようです。
並列処理できないthreadingモジュールを使う意味はあるのでしょうか。少なくとも5のように、IO待ち時間が長い処理を複数回行う場合はthreadingモジュールを使用する意味はあるようです。webページのクローラなどには向いているようです。
やはりthreadingモジュールよりもmultiprocessingモジュールを使用した方がいい気がする。(もしくはos.forkを使用するか)
参考
multiprocessing — Process-based parallelism — Python 3.8.0a4 documentation
2.6に新搭載のmultiprocessingを見て俺のPythonがおっきした件 | TRIVIAL TECHNOLOGIES 4 @ats のイクメン日記
Python 2.6 multiprocessing package を触ってみた。 [GIL回避] | Tricorn Tech Labs
並行コンピューティング技法 ―実践マルチコア/マルチスレッドプログラミング
- 作者: Clay Breshears,千住治郎
- 出版社/メーカー: オライリージャパン
- 発売日: 2009/12/21
- メディア: 大型本
- 購入: 12人 クリック: 598回
- この商品を含むブログ (38件) を見る