Python (CPython) のthreading.Threadは、GIL(Global Interpreter Lock)の影響で同時に2つ以上のスレッドが並列で動作しないらしい(並行では動作するけど)。
では、他の実装ならどうなのかと思い、Jythonで確認してみました。あと、JythonはJavaのAPIも使用できるので、java.lang.Threadもついでに確認してみた。
#検証環境
- CPU: Intel(R) Celeron(R) CPU N2830 @ 2.16GHz × 2
- Ubuntu 16.04
- Python
- Python 2.7.12
- Jython
- Jython 2.7.0
- OpenJDK 1.8.0_91 64bit ServerVM
#検証用ソースコード
4〜100,000の範囲の素数を列挙するタスクを分散して計算するワーカーを作成。以下がthreading.Threadを使った場合。
from threading import Thread
class Worker(Thread):
def __init__(self, start, end):
super(Worker, self).__init__()
self._start = start
self._end = end
def run(self):
self.prime_nums = []
for i in xrange(self._start, self._end):
if not 0 in self._remainders(i):
self.prime_nums.append(i)
def _remainders(self, end, start=2):
for i in xrange(start, end):
yield end % i
以下がjava.lang.Threadを使った場合。(importするクラスが異なるだけ)
from java.lang import Thread
class Worker(Thread):
def __init__(self, start, end):
super(Worker, self).__init__()
self._start = start
self._end = end
def run(self):
self.prime_nums = []
for i in xrange(self._start, self._end):
if not 0 in self._remainders(i):
self.prime_nums.append(i)
def _remainders(self, end, start=2):
for i in xrange(start, end):
yield end % i
そして、ワーカースレッドをキックして経過時間を測る処理が以下です。
import sys
from threading import Thread
from datetime import datetime
def total_seconds(td):
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
if __name__ == '__main__':
argv = sys.argv
argc = len(argv)
if argc < 4:
print 'ERROR: <worker_module> <n_workers> <max_value>'
sys.exit(1)
worker_module = argv[1]
n_workers = int(argv[2])
max_value = int(argv[3])
min_value = 4
interval = (max_value - min_value) / n_workers
Worker = __import__(worker_module).Worker
workers = []
for start in xrange(4, max_value, interval):
print 'Worker: %s, %s' % (start, start+interval)
worker = Worker(start, start+interval)
workers.append(worker)
start_time = datetime.utcnow()
for worker in workers:
worker.start()
for worker in workers:
worker.join()
end_time = datetime.utcnow()
elapsed_time = end_time - start_time
elapsed_sec = total_seconds(elapsed_time)
n_primes = sum([len(w.prime_nums) for w in workers])
print '# of primes = %s, time = %s sec' % (n_primes, elapsed_sec)
#結果
ワーカー処理が終わるまでの経過時間は以下の通りとなりました。
実装 | クラス | 1 thread | 2 threads |
---|---|---|---|
Python | threading.Thread | 100 sec | 125 sec |
Jython | threading.Thread | 101 sec | 73 sec |
Jython | java.lang.Thread | 101 sec | 77 sec |
Pythonは、同時に1つのスレッドしか動作できないので、2スレッドで分散しても速くならない (むしろ遅くなってる) ですが、Jythonでは違う結果になりました。
1スレッドでの経過時間はPythonとJythonでほとんど同じなので、今回用いた処理に対する基本的な性能は変わらないと見ています。(本当はJavaの動的コンパイルが効いてJythonの方が速くならないかなーと期待したのですが)
そして、Jythonの場合、1スレッドより2スレッドの方が早く終わったので、ちゃんと並列で動作してくれている感じです。
ここらへんの動作は実装依存なのかな。