search
LoginSignup
1

More than 1 year has passed since last update.

posted at

コルーチンとconcurrent.futuresの備忘録

はじめに

Twitterで一時期流行していた 100 Days Of Code なるものを先日知りました。本記事は、初学者である私が100日の学習を通してどの程度成長できるか記録を残すこと、アウトプットすることを目的とします。誤っている点、読みにくい点多々あると思います。ご指摘いただけると幸いです!

今回学習する教材

  • Effective Python

    • 8章構成
    • 本章216ページ
  • 第5章:並行性と並列性

多くの関数を並行に実行するにはコルーチンを考える

スレッドには、以下の3つの問題があります。

  • コードが複雑になる
  • スレッド1つの実行に約8MB という多量のメモリを必要とし、数万の関数を実行することは困難
  • スレッドの開始にコストがかかる

Pythonでは、これらの問題をコルーチン(処理を中断、再開できる構造のこと)で回避できます。
コルーチンは、ジェネレータの拡張として定義され、たった1KB以下のメモリで関数呼び出しから終了まで実行します。コルーチンで並列的振る舞いを真似ることができるのは、多くの別々のジェネレータ関数を次のyield式までそれぞれ進めておくことができるからです。

コルーチンでは、ジェネレータから値を取得するコードが、yield式を実行した直後のジェネレータに値を戻せます。コルーチンの例を示します。

def my_coroutine():
    while True:             
        received = yield             # sendメソッドで渡された値がyieldの値になる
        print('Received:', received)

it = my_coroutine()
next(it)              # コルーチン開始
it.send('First')     
it.send('Second')
# Received: First
# Received: Second

nextへの呼び出しをすることで、sendを受け取る準備をしています。
この準備をしないと、以下のようなエラーが出ます。
TypeError: can't send non-None value to a just-started generator

本当の並列性のためにconcurrent.futures を考える

Pythonの組み込みモジュールconcurrent.futuresのmultiprocessingを使うことで、複数CPUを活用し、子プロセスとして並列実行することができます。子プロセスは、それぞれGILも別になっているので、相互排他ロックのせいで逐次処理になることはありません。

例として、2数の最大公約数を見つける計算を逐次処理と、並列処理で実行してみます。

def gcd(pair):
    a, b = pair
    low = min(a, b)
    for i in range(low, 0, -1):
        if a % i == 0 and b % i == 0:
            return i

def main():
    numbers = [(12323221, 12341235), (82325471, 34352234),
            (38542512, 21384512), (29346852, 58192122)]
    start = time.time()
    results = list(map(gcd, numbers))
    end = time.time()
    print('Sequential: %.3f seconds' % (end - start))

    start = time.time()
    pool = concurrent.futures.ProcessPoolExecutor(max_workers=2)
    results = list(pool.map(gcd, numbers))
    end = time.time()
    print('Multi Process: %.3f seconds' % (end - start))

if __name__ == '__main__':
    main()

実行結果

Sequential: 6.036 seconds
Multi Process: 4.034 seconds

2倍とまでは行かないものの、1.5倍近く処理が速くなりました。
ただし、この方式は、分離した(プログラムの他の部分と状態を共有する必要が無い)、レバレッジの高い(少量のデータだけを親と子のプロセス間でやり取りすれば、大量の計算が可能)タスクでしか効力を発揮しません。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1