PythonとJythonにおけるマルチスレッド処理の違い

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を使った場合。

```python:py_worker.py
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するクラスが異なるだけ)

```python:jy_worker.py
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
```

そして、ワーカースレッドをキックして経過時間を測る処理が以下です。

```python:main.py
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スレッドの方が早く終わったので、ちゃんと並列で動作してくれている感じです。
ここらへんの動作は実装依存なのかな。