Python 2.6 multiprocessing package を触ってみた。 [GIL回避]

oktCentOS,CPU,GIL,multiprocessing,Python,Ruby,thread,threading,サンプル,マルチスレッド,マルチプロセス,並列処理

oktです。

普段、並列処理のプログラムを書くことは滅多にないんですが、今後のために GIL(Global Interpreter Lock) による影響を軽く確認しておこうと思ったのと、Python2.6 で実装されたmultiprocessing パッケージに触れておこうという事でネタにしてみました。

PythonやRubyでマルチスレッドな並列処理を書こうと思ったらGILの問題にぶち当たります。
最近のサーバでは2コア4コアが当たり前なのですが、GILの問題があると複数のCPUを有効利用できません。それを以下の言語とサンプルコードを使って状況確認します。

サンプルコードは、0から100,000,000までインクリメントするスレッドを4つ作成し、すべてのスレッドの完了を待って終了するものです。
動作環境はCentOS5(VMware Server上で動作)で仮想CPUを2個に設定しています。

使用する言語は下記の5つ。

  • Python 2.4.3
  • Python 2.6.1
  • Python 3.0
  • Ruby 1.8.5
  • Ruby 1.9.1-p0

Python2.4.3/2.6.1用threadingパッケージ利用時のサンプルコード(test.py)

from threading import *

class TestThread(Thread):
        def run(self):
                i = 0
                while i < 100000000:
                        i = i + 1

if __name__ == "__main__":
        mainthread = currentThread()
        for i in range(4):
                thread = TestThread()
                thread.start()
        for t in enumerate():
                if mainthread != t:
                        t.join()

Python3.0用threadingパッケージ利用時のサンプルコード(test-3.py)

from threading import *

class TestThread(Thread):
        def run(self):
                i = 0
                while i < 100000000:
                        i = i + 1

if __name__ == "__main__":
        mainthread = current_thread()
        for i in range(4):
                thread = TestThread()
                thread.start()
        for t in enumerate():
                if mainthread != t:
                        t.join()

Ruby1.8.5/1.9.1-p0用サンプルコード(test.rb)

for num in 1..4
    Thread.start(0) do |s|
        n = s
        while n < 100000000
            n = n + 1
        end
    end
end
Thread.list.each do |s|
    if Thread.current != s
        s.join
    end
end

Python2.4.3/Python 2.6.1/Python 3.0で実行した時のtopコマンドの表示は下記のようになりました。
top - 16:20:12 up 17:47, 4 users, load average: 0.31, 0.07, 0.02
Tasks: 55 total, 1 running, 54 sleeping, 0 stopped, 0 zombie
Cpu0 : 2.0%us, 43.0%sy, 0.0%ni, 55.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu1 : 15.2%us, 62.6%sy, 0.0%ni, 22.2%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 515444k total, 500240k used, 15204k free, 64172k buffers
Swap: 1048568k total, 4k used, 1048564k free, 369836k cached
 
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
28398 okt 17 0 27408 2464 1460 S 123 0.5 0:07.09 /usr/bin/python ./test.py

3バージョンともCPUの利用傾向に差は見られません。
system のCPU利用率が高いのはfutex(2)でのスレッド制御にCPUを消費しているからのようです。

Ruby1.8.5/Ruby1.9.1-p0で実行した時のtopコマンドの表示は下記のようになりました。
top - 16:22:13 up 17:49, 4 users, load average: 0.31, 0.07, 0.02
Tasks: 55 total, 3 running, 52 sleeping, 0 stopped, 0 zombie
Cpu0 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu1 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 515444k total, 499620k used, 15824k free, 64148k buffers
Swap: 1048568k total, 4k used, 1048564k free, 369836k cached
 
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
28395 okt 25 0 13776 1856 1088 R 101 0.4 0:07.98 /usr/bin/ruby ./test.rb

こちらはPythonと違い、userのCPU使用率が100%になっています。
Ruby1.9.1-p0は configure時に '—-enable-pthread’ をつけて作成したのですが
Pythonほど過剰にfutex(2)をコールしていないようで、systemのCPU使用率はかなり低くなっています。

こんな具合でPythonとRubyで違いはあれど、複数のCPUを効率的に使えていないのが分かります。
メモリの利用効率等を別にしてCPUでの処理効率を重要視するなら、マルチプロセスで並列処理をするか、GIL的な機構を採用していない言語を使う必要があるようです。

ちなみに、上記の5つのケースでtimeコマンドを使用して実行時間をみるとこんな感じになりました。

Python 2.4.3(threading利用):
$ /usr/bin/python -V
Python 2.4.3
$ time /usr/bin/python test.py
real 1m16.848s
user 0m13.027s
sys 1m40.013s

Python 2.6.1(threading利用):
$ /usr/local/bin/python -V
Python 2.6.1
$ time /usr/local/bin/python test.py
real 1m4.873s
user 0m10.973s
sys 1m20.533s

Python 3.0(threading利用):
$ /usr/local/python3000/bin/python -V
Python 3.0
$ time /usr/local/python3000/bin/python test-3.py
real 1m26.549s
user 0m29.197s
sys 1m29.717s

Ruby 1.8.5:
$ /usr/bin/ruby -v
ruby 1.8.5 (2006-08-25) [i386-linux] $ time /usr/bin/ruby test.rb
real 1m30.336s
user 1m30.301s
sys 0m0.028s

Ruby 1.9.1-p0( –enable-pthread):
$ /usr/local/ruby191p0/bin/ruby -v
ruby 1.9.1p0 (2009-01-30 revision 21907) [i686-linux] $ time /usr/local/ruby191p0/bin/ruby test.rb
real 0m11.447s
user 0m11.367s
sys 0m0.040s

Ruby 1.9.1-p0 が圧倒的に実行時間が短いですね。

で、ここからが本題です(前置き長すぎ)。
先ほどのPythonのthreadingパッケージを使ったマルチスレッド処理を、Python 2.6から実装されたmultiprocessingパッケージを利用したものに改変してみましょう。
Python multiprocessing 利用のサンプルコード(test-mp.py)

from multiprocessing import *

class TestThread(Process):
        def run(self):
                i = 0
                while i < 100000000:
                        i = i + 1

if __name__ == "__main__":
        for i in range(4):
                thread = TestThread()
                thread.start()
        for t in active_children():
                t.join()

Python2.x threadingパッケージ利用時のサンプルコード(test.py)との差異はほとんどありません。
何点か置換するだけで済みました。

上記のサンプルコードをPython2.6.1で実行した時のtopコマンドの表示は下記のようになりました。
top - 16:23:23 up 17:50, 4 users, load average: 0.31, 0.07, 0.02
Tasks: 57 total, 3 running, 54 sleeping, 0 stopped, 0 zombie
Cpu0 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu1 :100.0%us, 0.0%sy, 0.0%ni, 0.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 515444k total, 496512k used, 18932k free, 42392k buffers
Swap: 1048568k total, 4k used, 1048564k free, 394228k cached
 
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
5740 okt 25 0 8148 2264 424 R 50 0.4 0:01.13 /usr/local/bin/python test-mp.py
5741 okt 25 0 8148 2264 424 R 50 0.4 0:01.12 /usr/local/bin/python test-mp.py
5742 okt 25 0 8148 2264 424 R 50 0.4 0:01.13 /usr/local/bin/python test-mp.py
5743 okt 25 0 8148 2264 424 R 50 0.4 0:01.12 /usr/local/bin/python test-mp.py
5739 okt 20 0 8148 3540 1700 S 0 0.7 0:00.03 /usr/local/bin/python test-mp.py

当たり前ですが、Cpu0,Cpu1の両方が100%フルに使われているのが分かります。
ちなみに、timeコマンドを使用して実行時間をみると下記のようになりました。

Python 2.6.1(multiprocessing利用):
$ /usr/local/bin/python -V
Python 2.6.1
$ time /usr/local/bin/python test-mp.py
real 0m12.125s
user 0m23.841s
sys 0m0.120s

単純に、Python 2.6.1でthreadingを利用したケースと比較すると大幅に高速化されました。
すべてのパターンに当てはまるわけではないですが、処理内容によっては、
リソースに余裕があるのであればmultiprocessingを使った方が高速に処理できそうです。
multiprocessingの利点は他にもあるのですが、それはまたの機会に。