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を使用するか)