ページ

2011年12月13日

pypyのスレッドとかgreenletとか何か

pypy advent calendarのエントリです。ごめんなさい。ネコを飼うのでそのことに関心が向いて、pypyのこと、すっかり忘れていました。なので、いつにも増して、とってもうすーいエントリをだらだらと書きます。pypy闇の軍団長兼会長様、許してください。

まずは、昔のフィボナッチ数列の計算するコードを持ち出します。
import time

def fibonacci(n):
    value = 0
    f1 = 1
    f2 = -1
    for i in range(n+1):
        value = f1 + f2
        f2 = f1
        f1 = value
    
    return value

if __name__ == "__main__":
    for x in range(3):
        fibonacci(3)
    time.sleep(0)
    start = time.time()
    for x in range(4000, 5000):
        fibonacci(x)
    end = time.time()
    print end - start

これを実行すると、僕のMacBook Airだと今日は大体1.11秒です。Java(Jython?)などのちゃんとしたスレッドを持ってる人たちは、ループの中をスレッドにするともっと高速になるんじゃないか?って考えます。なので、スレッドにしちゃいましょう。
import time
import threading

def fibonacci(n):
    value = 0
    f1 = 1
    f2 = -1
    for i in range(n+1):
        value = f1 + f2
        f2 = f1
        f1 = value
    
    return value

if __name__ == "__main__":
    for x in range(3):
        fibonacci(3)
    time.sleep(0)
    thread_list = []
    start = time.time()
    for x in range(4000, 5000):
        thrd = threading.Thread(target=fibonacci, args=(x,))
        thread_list.append(thrd)
        thrd.start()
    for thrd in thread_list:
        thrd.join()
    end = time.time()
    print end - start

さて、これを実行すると1.7秒ぐらいです。参考までにCPythonで実行すると2.01秒でした。さらに参考までにPython3で実行すると2.37秒でした。Python3ダメダメじゃん。
(;゜〇゜)はっ!これはPython3のエントリじゃないからこれ以上Python3のことを書いたら団長に怒られる!
さて、スレッドにした方が遅いですね。まあ、何も不思議じゃなくって、GILとかあるので、想定の範囲内です。ちなみに、1000個スレッドがあると、LinuxやMacはメモリは見かけ上8Gぐらいなります。まあ、単純な計算だけならスレッドにするより何も考えずにシーケンシャルに行った方が良いと言うことですね。もしくはプロセスのフォークで・・・。でも、途中でネットワークとかブロックするような処理があると、シーケンシャルにやると遅くなっちゃいます。

ここまでが前置きです。結論はないです。Application-level Stackless featuresにcontinuletとgreenletについて書かれています。どっちもマイクロスレッドですね。continuletのレイヤの上にgreenletと同じAPIのモジュールを実装しているようです。32bit環境ではスレッドあたり4KBのメモリ使用量だって書いています。本物のスレッドに比べると遙かに少ないです。
と言うことで、こんなコードを書いてみました。
import time
from greenlet import greenlet

def fibonacci(n):
    value = 0
    f1 = 1
    f2 = -1
    for i in range(n+1):
        value = f1 + f2
        f2 = f1
        f1 = value

    return value

if __name__ == "__main__":
    parent = greenlet()
    start = time.time()
    grs = [greenlet(fibonacci, parent) for _ in range(1000)]
    i = 4000
    for gr in grs:
        gr.switch(i)
        i += 1
    end = time.time()
    print end - start
実行すると1.2秒ぐらいです。最初のコードが1.1秒ぐらいなので0.1秒ぐらいのオーバーヘッドですね。まあ、待ちがないマイクロスレッドでスレッドの途中でコンテキストの切り替えがないので、つまんないですね。

ちょっとこれはいろいろやり過ぎているので、もっと単純なコードでやってみましょう。
import threading
import time


def test2():
    pass

start = time.time()
thrds = [threading.Thread(target=test2) for i in range(1000)]
for thrd in thrds:
    thrd.start()
for thrd in thrds:
    thrd.join()
end = time.time()

print end - start

単にスレッドを作って空の関数を実行しているだけです。これだけのコードで0.29秒です。感想としては、遅い・・・。CPython2.7だと0.14秒です。うっ、pypy負けてる。と言うかJITがうまく動かなさそうなコードなのでいいです。ちなみにPython3でも0.14秒ぐらいでした。
それではgreenletで。
from greenlet import greenlet
import time

def test1(x, y):
    for gr in grs:
        z = gr.switch(2)

def test2(u):
    #print id(greenlet.getcurrent())
    gr1.switch(42)

start = time.time()
gr1 = greenlet(test1)
grs = [greenlet(test2) for x in range(1000)]
gr1.switch(1, 2)
end = time.time()

print end - start

このヒトを実行すると0.07秒です。スレッド重いですね。

2011年12月9日

Python3のconcurrentパッケージ

こんな経緯Python Advent Calendarに参加させられてしまいました。まずは、言い訳から。僕がPython3を触ったことがあるのは2,3年前で、それも数日だけです。それ以降全く触っていません。はい。Python3初心者です。笑いたければ笑ってください。そんな僕がPython3で何を書こうか、と言うことで、あのみんなが大好きなJavaからぱくったと名高いconcurrentパッケージについて書きます。
まず、まっとうなPythonプログラマならあのレガシーなJavaぐらい分かるよね。Javaが分かるんだったらJavaのjava.util.concurrentパッケージぐらいは知っていて、concurrentて常識だよね、と言うことで、concurrentについての説明は多分、省略です。

PythonのconcurrentパッケージはJavaのconcurrentパッケージのほんの一部です。って言うか、executer関連しかないじゃん・・・。concurrentパッケージの下にはfuturesしかいないし・・・。でも、今後増えそうな感じがして頼もしくはあります。Python3でGILがどうなっているか知りませんが、pypyやjythonなどもあるので、スレッド周りの安全性を実装系に依存しちゃうのはあまりうれしくありません。なので、スレッドの安全性は意識した方が良いと思うわけです。でも、そんな能書きはいいのでゆっくりとコードと戯れてみましょう。

まずは、concurrentを使わないコードです。もっと最初はスレッドを使わないモノが必要ですが、めんどいのでごめんなさい。
import threading
import time

def thread_func():
    ident = threading.currentThread().ident
    print("start")
    for i in range(4):
        print("waiting " + str(i))
        print("current thread: " + str(ident))
        time.sleep(0)
    print("done")

l = [threading.Thread(target = thread_func) for i in range(4)]
for thrd in l:
    thrd.start()
for thrd in l:
    thrd.join()

スレッドをぐるぐる回して、最後にjoinしてすべてのスレッドが終わるのを待っています。これを実行するとスレッドが切り替わっているのが分かります。ループ内でちょっと重い処理をして、お互いに依存関係がないときに、こんな感じのコードは便利です。

でも、今回はスレッドというかループ数が4だから良いけど、もっと多いと一瞬で破綻しちゃうかもしれません。LinuxとかMacはデフォルトのままだとスレッド一つにつきメモリを8M消費します。こんなところで大量にメモリを消費すると困っちゃいます。と言うことで、大体スレッドプールとかを実装するわけです。まあ、みんな同じことを考えます。と言うことでDRYって怒られそうです。怒られてしょぼくれているあなたにconcurrent.futuresのexecuterです。まずは、ThreadPoolExecuterです。名前からしてそれっぽいでしょ。実行して貰えれば、スレッドが最大二つ(max_worker=2)しか動かないです。そして、別のスレッドで動いているのが分かると思います。でも、workerが一つしかいないよ!って言う人は、もっと沢山submitしてあげれば大丈夫です。

import threading
import concurrent.futures
import time

def executer_func():
    ident = threading.currentThread().ident
    print("start")
    for i in range(4):
        print("waiting " + str(i))
        print("current thread: " + str(ident))
        time.sleep(0)
    print("done")
    return ident


with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executer:
    l = [executer.submit(executer_func) for i in range(4)]
    for future in l:
        print("=== result: " + str(future.result()))

ThreadPoolExecutorを使うとスレッド数を制御・プールできて使い回せます。スレッドが終了したら、未来のオブジェクト(future)から結果を取り出せます。えっ、PythonはGILがあるから本当に重い処理はコアを生かしきれないって?わがままだな。そんなにCPUを発熱させたければ、ProcessPoolExecuterです。
import threading
import os
import concurrent.futures
import time

def executer_func():
    ident = threading.currentThread().ident
    print("start")
    for i in range(4):
        print("waiting " + str(i))
        print("current thread: " + str(ident))
        time.sleep(0)
    print("done")
    return os.getpid()


with concurrent.futures.ProcessPoolExecutor(max_workers=2) as executer:
    l = [executer.submit(executer_func) for i in range(4)]
    for future in l:
        print("=== result: " + str(future.result()))
ほら、これでスレッドじゃなくプロセスとして実行されます。CPUをこき使ってください。
でもなー、まあ、待たなくてもいいので、実行した結果がだけをもうちょっと処理したいんだけど・・・、って誰かが文句をたれています。Javaだと・・・、ってすぐにJavaを引き合いに出す人がいます。大丈夫です。タスクが終わったらコールバックを呼ぶことができます。Javaだと何かとオブジェクトを作ってめんどいですが、Pythonはそんなことは在りません。
import threading
import concurrent.futures
import time

def executer_func():
    ident = threading.currentThread().ident
    print("start")
    for i in range(4):
        print("waiting " + str(i))
        print("current thread: " + str(ident))
        time.sleep(0)
    print("done")
    return ident

def done_callback(future):
    print("=== result: " + str(future.result()))

with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executer:
    for i in range(4):
        future = executer.submit(executer_func)
        future.add_done_callback(done_callback)

futureにadd_done_callbackで処理が終わったときに実行する処理を指定できます。
まあ、大体分かったけど、発行したタスクが全部がちゃんと終わってから何かをしたいって?じゃあ、waitすればいいじゃん。と言うことで、

import threading
import concurrent.futures
import time

def executer_func():
    ident = threading.currentThread().ident
    print("start")
    for i in range(4):
        print("waiting " + str(i))
        print("current thread: " + str(ident))
        time.sleep(0)
    print("done")
    return ident


with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executer:
    l = [executer.submit(executer_func) for i in range(4)]
    results = concurrent.futures.wait(l)
    for result in results.done:
        print("=== result: " + str(result.result()))
concurrent.futures.waitですべてが終わるのを待ってくれます。その後、処理が終わったものを取り出して(result.done)、結果を処理するよろし。

executerのmapについても書こうかと思ったのですが、pythonの組み込み関数のmapと大体同じで面白くないのでやめました。
終わったモノから順番になるべく速く処理するときはconcurrent.futures.as_completedで取り出せます。

と言うことで、concurrentでした。本音を言えば、ループをみてJITがスレッドにできるところは勝手にスレッドにしてくれて、何もしていないのにある日突然、アプリケーションが高速化してくれるのが理想です。インテルのスレッディングビルディングブロックみたいに・・・。まあ、そこまで行かなくてもヒント(アノテーションとか)を書いておけばJITが解釈しやすくなるとかでも・・・。

次はaodag先生です。
でわでわ

2011年12月6日

Webページからファイルをデスクトップにドラッグアウトする

Gmailは、ChromeやFirefoxだと添付ファイルをデスクトップにドラッグアンドドロップで保存できます。IE? 何それ? おいしいの? なので、IEのことはよく知りません。IE8だと動いてくれませんでした。(Firefoxもウソだ。やっぱり動かなかった。)このページにその方法が書かれています。Drag and Drop APIっていうのを使うらしいです。とりあえず、サンプルコード。

<html>
  <body>
    <a href="some.png"
       draggable="true"
       ondragstart="event.dataTransfer.setData('DownloadURL',
        'image/png:some.png:http://localhost:8080/some.png');">
      file link
    </a>
  </body>
</html>

これだけです。リンクをドラッグアンドドロップえきるようにして、ondragstartイベントでdataTransferにsetDataするだけです。セットするのはmime typeとファイル名、ファイルへのリンクです。mime-typeはapplication/octet-streamでも大丈夫です。ファイルへのリンクは相対パスではダメで、絶対パスで書かないと行けないのがめんどいです。でも、これだけでデスクトップへのドラッグアンドドロップでの保存ができるので、簡単です。
でも、僕はドラッグアンドドロップではほとんど保存しないですが…

でわでわ