はじめに
Twitterで一時期流行していた 100 Days Of Code なるものを先日知りました。本記事は、初学者である私が100日の学習を通してどの程度成長できるか記録を残すこと、アウトプットすることを目的とします。誤っている点、読みにくい点多々あると思います。ご指摘いただけると幸いです!
今回学習する教材
-
- 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倍近く処理が速くなりました。
ただし、この方式は、分離した(プログラムの他の部分と状態を共有する必要が無い)、レバレッジの高い(少量のデータだけを親と子のプロセス間でやり取りすれば、大量の計算が可能)タスクでしか効力を発揮しません。