2011-02-08

Ajaxを補足するためのPythonによる手軽なマルチスレッド処理

Webアプリの開発で、重い処理はAjaxで別途処理して取得するという手段が取れない場合があります。

「画面の一部で矢印がぐるぐる回って待たせるのではなく、本当に一瞬で全部表示したい」

などの場合です。

そんなときにお手軽に使えるテクニックとして、サーバー処理のマルチスレッドによる並列化があります。
Pythonのマルチスレッドでは、高度なCPUの並列処理はできませんが(グローバルインタプリタロックと言います)、少なくともI/O待ちの間別の処理を流す程度の事は可能です。[参考]
その特徴を生かして、Webアプリのレスポンスを速めることができます。


これを使う場面ですが、たとえば、以下のようなものがあります。



この画面はうちの会社で作っている特許分析WEBアプリのBiz Cruncherというもので、特許の詳細な内容を表示しているところです。


特許の閲覧というのは企業知財部では古来から「手めくり」と呼ばれ、いかに速く大量の特許の内容を把握するかに心血を注ぎます。
そのため、図などは一瞬で見れるべきだし、電子的な画面であろうと、一瞬で次々表示されなければストレスを感じるものです。


そういう場合は付属情報はAjax化で別リクエストにするにしても、必須の要素だけは泣く泣く1リクエストに押し込んで、極限まで速度を向上させる必要があります。

たとえば、上の特許詳細画面では時間のかかる処理として、テキストマイニング、画像の生成、詳細テキスト(長文)の取得、引用情報・経過情報の取得 etc.をそれこそ一瞬で行う必要がありました。

改良前はこの表示に2.8秒かかっていたのですが、それでも不満ということで、冒頭のマルチスレッド化を行って0.7秒にまで縮めることができました。

具体的には以下の関数 call_async を使います。

import threading
def call_async(target, args=None, kwargs=None):
u"""
関数非同期呼び出しツール。
関数を別スレッドで呼び出して、その後結果を取得できます。

#使用例
r = call_async(f,args=(5,))
#ここで別の処理を片付けます。
r["thread"].join() #スレッドが終わるのを待ちます。
if "result" in r:
print "RESULT", r["result"]
else:
print "ERROR", str(r["exception"])
"""
res = {}
def f(*inargs, **inkwargs):
try:
inargs[-1]["result"] = target(*(inargs[:-1]), **inkwargs)
except Exception, e:
inargs[-1]["exception"] = e
res["thread"] = threading.Thread(target=f, args=(args+(res,)), kwargs=kwargs)
res["thread"].start()
return res



この関数を使うと、関数呼び出しを別スレッドにしながら、その返り値を取得することもできます。
使用方法は上記関数のコメント内にありますが、以下のように、先に遅い処理を実行開始しておき、平行してその他の処理を済ませたあとで、ゆっくりと結果を取得します。

#使用例 slow1~3という遅い処理の関数がある場合

#まず冒頭で遅い処理を別スレッドで実行開始しておきます。
r1 = call_async(slow1,args=(5,)) #遅い処理1を開始!
r2 = call_async(slow2,args=("ABC","DEF")) #遅い処理2を開始!
r3 = call_async(slow3,args=([1,2,3,4])) #遅い処理3を開始!

#ここでその他の処理を片付けます。

r1["thread"].join() #スレッドが終わるのを待ちます。
r2["thread"].join() #スレッドが終わるのを待ちます。
r3["thread"].join() #スレッドが終わるのを待ちます。

if "result" in r1: #slow1の結果を使う処理
print "RESULT 1", r1["result"]
else:
print "ERROR 1", str(r1["exception"])

if "result" in r2: #slow2の結果を使う処理
print "RESULT 2", r2["result"]
else:
print "ERROR 2", str(r2["exception"])

if "result" in r3: #slow3の結果を使う処理
print "RESULT 3", r3["result"]
else:
print "ERROR 3", str(r3["exception"])


こうすることで、I/O待ちの発生する処理をいくつも並列で実行でき、実行時間の効果的な短縮を実現できます。

こうした手段としてPython標準のmultiprocessingが検討にあがりますが、あれはより高度な分散処理用ですので、関数の結果を取得しようとすると、とたんに大仰なものになります。

また、もちろんですが、この秒数の短縮で効果的なのはJavascriptの最適化もあるわけです。
それをした後に、さらにどうしてもサーバー側も高速化しないといけない場合は、このお手軽なマルチスレッド化も考慮するとよいと思います。