JavaとかCとかでマルチスレッドとか書いたことはあったけど、最近使ってるPythonでも並列処理したいなぁと言うことで、Python3環境で手軽にマルチプロセス並列処理して速度を上げてみた。
ベース環境はお名前.com VPSのCentOS6で、SCLのPYTHON35環境、Python3.5.1。
まずは下準備で、pipでjoblibを入れる。 更にその準備でpythonのdevelパッケージを入れておく。
1 2 |
yum install rh-python35-python-devel pip3.5 install joblib |
で、今回作ったもの。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#! /usr/bin/env python # -*- coding: utf-8 -*- import sys import re import MySQLdb import math import more_itertools from joblib import Parallel, delayed def checker(lines: list): sql = 'select * from filelist where size=%s' conn = MySQLdb.connect(user='mysqluser',passwd='mysqlpassword',host='mysqlhost',db='mysqldb', charset='utf8') cur = conn.cursor() cur.execute('SET NAMES utf8') ret = [] for line in lines: if line != None: m = re.search('[ ]*([0-9]+)[ ]+([^ ].+)$', line) if m: cur.execute(sql % m.group(1)) for row in cur.fetchall(): ret.append(row['id']) cur.close() conn.close() return ret f = open(sys.argv[1]) lines = f.readlines() linesdigit = int(math.log10(len(lines))) if linesdigit>1: linesdivider = pow(10,linesdigit-1) else: linesdivider = 1 ret = Parallel(n_jobs=-1)( [ delayed(checker)(linesgroup) for linesgroup in more_itertools.chunked(lines, linesdivider) ] ) print(ret) |
Joblibで並列処理する一般的な定型は、
Parallel(並列度)(
[
delayed(関数)(関数の引数)
for delayedに渡す引数 in ~
]
)
みたいな形。 並列度は-1でオート。
Parallelで並列実行する。
関数呼び出しになるんで、並列処理したい部分を関数にまとめて定義、今回はcheckerを作成。
Parallelの中では、delayedで関数呼び出しの形を書いて、forでdelayedを繰り返し呼び出しするための引数を用意をする。
今回はchecked関数にlinesgroupを引数として実行、linesgroupはlinesをchunkedを使って適当な長さに切り分けたもの。
で、checkerの中ではそれぞれMySQLに繋いで部分要素を検索して、一致した物のidをリストにして戻り値に返す。
Parallel自体の戻りは全てのdelayed呼び出しが完了するまで待機される。
Parallelの戻りは、リストで各delayedの戻り値が並ぶ。
[delayed1の結果, delayed2の結果…] → この場合は、[[1,2,3],[4,5,6]…]みたいな形でリスト内にリストが帰る形になって来る。
この場合はMySQLに繋いでいるので、正規表現部分の演算処理とMySQL側入出力の待機処理の両方がかかってるけど、joblibの場合は演算ばっかりでも待機ばっかりでもどちらにも効果がある。
→集計や加工等を繰り返す演算量が多い場合に、並列で実行することで実時間を短縮出来る。 ちょっとしたツールでも有効。
→クエリ的にDBの応答が遅い場合等の外部要因で、必要になるデータ要求が複数あるなら並列で投げ込んでやることでDB側が並列処理になって遅延を隠蔽できる。 WebアプリやDBに細工するようなプログラムでも有効。
実行してみる@お名前VPS2GB
1 2 3 4 |
time ./parallel.py list.tsv real 1m40.516s user 2m58.304s sys 0m3.012s |
realが現実時間で何秒かかったか、user・sysはCPUを何秒使ったか(1コアを10秒使うとCPUタイムが10秒、2コア同時に10秒間使うと合計で20秒になる。 sysはオーバヘッド)
この場合、1分40秒で応答が帰ってきたけど、CPUは2分58秒+3秒分使った。
普通に書けば3分くらいかかる処理の結果が1分40秒で得られたと言う事で結構な高速化。
必死にプロセス管理とかしないでも、スクリプトで簡単に並列処理が実行できて便利。
但し、プロセスが独立して立つからオーバヘッドもアリ、ある程度大きな物に使わないと効率は悪くなる。
プロセスなのでメモリはコピーされて立つ。
→立つプロセス分メモリ消費になるのでリソースに注意。
→普通にメモリ(変数)を書き換えても元のプロセスには反映されない。 戻り値にまとめるのが簡単、共有メモリを使う方法もある(multiprocessing.Value)
共有メモリの場合は、どのタイミングでどのプロセスが変更しているかわからんので、それを管理する必要があり面倒、並列プログラミングは依存とかを見通してやらないとダメだね。
※Parallelの引数”backend”で”threading”を選べばマルチスレッドで動作して、オーバーヘッドは減るがPythonのGILにより通常の処理中は並列処理できない(GILをリリースする部分だけが並列で動作できる=DBアクセスの待ちなどが大きいプログラムの場合のみ有効で小さい場合は速度低下の恐れもある)、デフォルトは”multiprocessing”のマルチプロセス動作。
近年はCPUのクロック上昇は相当低くなっていて、コア数増加が主流。
クライアントでも4コアを越えて6コアとか8コアとかが普通に入手可能だし、サーバに至ってはソケットあたり20コア、筐体あたり40コア80スレッドなんて言うのもよく見かけるんで、並列プログラミングはエンジニアには重要なスキルだろう。
インフラのクラウド化などでは、並列性能で拡張していくことになるからやはり欠かせないだろう。
(3702)