2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はじめに

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

2
2
0

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
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?