"BOKU"のITな日常

還暦越えの文系システムエンジニアの”BOKU”は新しいことが大好きです。

PythonのThread(並列処理)は速度改善効果がないらしいので確認する。

PythonのThreadで並列性による速度改善効果を期待しても無駄だと教えてもらったので、実際に試してみて、本当ならどうすればよいのかもあわせて確認しておきます。

f:id:arakan_no_boku:20190427205325j:plain

 

普通のThread処理で速度改善効果を計測する

 

マルチスレッド対応のクラスを作成するやり方で、PythonのThreadを使ってみます。

それで、同じ重たい処理をシーケンシャルに実行するのと、Threadを使って並列処理するので、どのくらい速度があがるのか?を確認してみます。

まず、シーケンシャルの処理です。

import time


# 単に時間がかかるだけの処理
def killing_time(number):
    return_list = []
    for i in range(1, number + 1):
        if number % i == 1:
            if i <= 9999:
                return_list.append(i)
    return return_list


start = time.time()
num_list = [25000000, 20000000, 20076000, 14500000]
for n in num_list:
    result_list = list(killing_time(n))
stop = time.time()
print('%.3f seconds' % (stop - start))

killing_time(時間潰し)という名前通りの処理を4回ループで実行して、time()で時間計測しています。 

自分のPCでは平均「4.547秒」かかりました。

今度は、これをスレッドで処理してみます。

まずは、ソースから。

import time
import threading


class MyThread(threading.Thread):
    def __init__(self, number):
        super().__init__()
        self.number = number

    def killing_time(self, number):
        return_list = []
        for i in range(1, number + 1):
            if number % i == 1:
                if i <= 9999:
                    return_list.append(i)
        return return_list

    def run(self):
        self.factors = list(self.killing_time(self.number))


start = time.time()
threads = []
num_list = [25000000, 20000000, 20076000, 14500000]
for n in num_list:
    thread = MyThread(n)
    thread.start()
    threads.append(thread)
for th in threads:
    th.join()
stop = time.time()
print('%.3f seconds' % (stop - start))

Threading.Threadクラスを継承してMyThreadクラスを作ってます。

Threading.Threadを継承したクラスで、コンストラクタ「__init__()」で、親クラスのコンストラクタ「super().__init__()」を実行し、run()メソッドに、スレッド内で実行したい処理を書けば、Thread化可能なクラスになります。

MyThreadのインスタンスを生成し、start()すれば処理が各スレッド内で処理します。

各Thread内の処理がすべて終了するのを待つのは、この部分です。

for th in threads:
    th.join() 

動かしてみました。

普通に考えれば、シーケンシャルに実行していた4つの処理を、Threadで並列処理したわけなので、多少は実行時間が短くなるはずです。

 ところが、どっこい。

平均の実行時間は「4.493秒」・・・。

なるほど・・全く速くなっていません(笑)

 

Threading.Threadでは速度向上しないのが仕様らしい

 

納得いかない結果ですが、調べてみると、これがPythonの仕様です。

Pythonにはプログラム実行の一貫性を保証する仕組みとして「Global Interpreter Lock(GIL)」を持っています。

GILは、優先度の高いスレッドが実行中のスレッドに割り込んで制御を奪ってしまい一貫性が崩れることを防ぐ「相互排他ロック(mutex」機能らしいのですが、その副作用として、同時に1つのスレッドしか進行できないようにしてしまうみたいです。

つまり。

GILが動いている限り、マルチスレッドで動かしても、結局ループを回してシーケンシャルに処理するのと、変わらない動きをするから、今回のようにほぼ同じか、もしくはThread起動のオーバーヘッド分遅くなるのが関の山・・という、実に(;´д`)トホホな話でした。

どうも、PythonのThreadはシステムコールを行う際の「ブロッキングI/O」を扱うためにサポートされているとか・・、そもそも速度向上を狙うようににはなってないみたいなのですね・・。

 

並列処理で速度向上効果を実現する方法

 

Pythonで並列処理の効果で速度向上をはかるための機能は、他のところにありました。

concurrent.futures.ProcessPoolExecutor() です。

docs.python.org

これは上記の「Global Interpreter Lock(GIL)」を回避して、マルチプロセスの実行ができるようになっているらしいです。

とりあえず、やってみます。

まず、ソースです。

import time
import concurrent.futures


# 単に時間がかかるだけの処理
def killing_time(number):
    return_list = []
    for i in range(1, number + 1):
        if number % i == 1:
            if i <= 9999:
                return_list.append(i)
    return return_list


def main():
    start = time.time()
    num_list = [25000000, 20000000, 20076000, 14500000]
    with concurrent.futures.ProcessPoolExecutor(max_workers=4) as excuter:
        result_list = list(excuter.map(killing_time, num_list))
    stop = time.time()
    print('%.3f seconds' % (stop - start))


if __name__ == '__main__':
    main()

いろいろ、こまかいところで違いがあります。 

まず、「max_worker」の指定。

concurrent.futures.ProcessPoolExecutor(max_workers=4)

これは、CPUコア数とかを指定しとけばよいみたいです。

自分のPCは4コアなので、4にしました。

あと、マルチプロセスで処理を動かすところですが、Threadのように、ループで回したりする必要はないです。

excuter.map(killing_time, num_list)

 このように、ファンクション名と、引数になるリストを渡してやって、マルチプロセスに振り分けるのはおまかせって感じです。

あと注意点としては。

  • pickle 化できるオブジェクトしか実行したり返したりすることができない。
  • 対話的インタープリタでは動かない。

というのがあります。

なので、前の2つのソースのように横着に書くと、動きませんので、上記のソースでも「main()」でくくって、__name__のチェックとかをやってます。

などなど・・癖はありますが、実行結果は見事なもので。

1.769秒

きちっと並列処理されて、パフォーマンスアップしています。

 

まとめ

 

PythonJavaなんかと違って、Threadをバンバン立ち上げて並列性を使ってパフォーマンスを稼ぐ・・ような考え方では、うまくいかないことは、よくわかりました。

サーバーサイドとかで、シビアな条件で使うときには、意識しないとダメですね。

まあ。

個人的に使う分には、そもそも並行処理する必要性もあまりないんですけどね。

ではでは。