【Python】並列処理と並行処理を実装する方法【マルチプロセス・マルチスレッド】

  • 並列処理と並行処理とは?
  • マルチプロセスとマルチスレッドとは?
  • Pythonで並列処理と並行処理を実装するには?

本記事ではこのような疑問を解決します。


「複数の処理を同時に実行する」みたいな文言が出てきた場合、
ほぼ100%登場するのが並列処理と並行処理です。

並列処理または並行処理を実装すれば、複数の処理を同時orほぼ同時に実行することができます。

ただ、この並列処理と並行処理については少し難しく感じている方も多いでしょう。


そこで今回は並列処理と並行処理の説明からPythonにおける実装方法までわかりやすく解説します。


Pythonのコードだけ確認したいという方は、
並列処理・並行処理やマルチプロセス・マルチスレッドなどの説明を読み飛ばしてOKです!

並列処理と並行処理とは?

まずは並列処理と並行処理について理解していきましょう。


そもそもプログラムはコードの上から順番に処理が実行されます。

つまり、前の処理が終わってから次の処理が実行されるという単純な仕組みです。

このような処理の流れを逐次処理と言います。

イメージでいうと、1人の作業者が例えば処理Aを行い、完了したら処理Bに移り、また完了したら処理Cを行うといった感じです。


ただ、処理の中には待ち時間が発生することが多いです。

例えば、ある処理の中で待ち時間が発生した場合、その待ち時間の間に他の処理を進めた方が全体としての作業時間を短くできますよね。

こちらもイメージで表すと、1人の作業者がまずは処理Aを行い、待ち時間が発生したら処理Bに移り、処理Bの中で待ち時間が発生したらまたその間に処理Aか処理Cを行うといった感じです。

つまり、作業者は単一だが、処理の待ち時間に別の処理を行うという流れです。

このような処理の流れを並行処理と言います。

並行処理についてよく言われるのは、実際には同時に複数の処理を実行しているわけではないけども、待ち時間を有効活用しているため、表上では同時に複数の処理を実行しているように見えるということです。


そして、さらに処理を全体的に早く完了させたい場合は、当たり前な話ですが、作業者の数を増やしてしまえばいいわけです。

例えば、処理が3つある場合、3人がそれぞれ1つずつ処理を同時に行えば作業時間は早まります。

つまり、複数の作業者が同時に処理を行うといった流れです。

このような処理の流れを並列処理と言います。


それでは並列処理と並行処理をどのように実装すれば良いのかを見ていきましょう。

マルチプロセスとマルチスレッドについて

並列処理と並行処理を実装する手段としてあるのがマルチプロセスとマルチスレッドです。


マルチプロセスとは複数のプロセスを実行することを言います。

例えば、WordとExcelを起動すると、WordとExcelの2つのプロセスが立ち上がるといったイメージです。


一方、マルチスレッドは複数のスレッドを使うことを言います。

スレッドとはプロセスの中をさらに分割した処理の単位のことです。

つまり、1つのプロセスは1つまたは複数のスレッドを保持します。


Pythonの場合、
マルチプロセスを使うと並列処理を実装でき、
マルチスレッドを使うと並行処理を実装することができます。

並列処理と並行処理の使い分け

ここまで並列処理と並行処理について確認してきましたが、
両者をどのように使い分ければいいのか疑問になりますよね。


結論からいうと、
「処理の中に長い待ち時間が発生しない処理には並列処理、長い待ち時間が発生する処理には並行処理を使う」です。


処理の中に長い待ち時間が発生しない処理とはCPUリソースを多く消費するような処理です。

つまり、処理速度がCPUに依存する処理を指します。

例えば、数値計算などです。

このような処理をCPUバウンドな処理と言います。

CPUバウンドな処理には並列処理が向いています。


逆に、長い待ち時間が発生する処理とは処理の大半が主に入出力操作の完了の待機に費やされる処理のことです。

例えば、ファイルの読み書きやWeb APIの利用、ネットワーク通信などです。

このような処理をI/Oバウンドな処理と言います。I/Oバウンドな処理には並行処理が向いています。


ここまでをまとめると、
並列処理はマルチプロセスで実装できてCPUバウンドな処理に向いています。

一方、並行処理はマルチスレッドで実装できてI/Oバウンドな処理に向いています。

Python GILについて(※読み飛ばしOK)

こちらは話が少しマニアックになるため、必要ない方は読み飛ばしてもらっても大丈夫です。


マルチスレッドを扱う時、
Python(※厳密には後述するCPython)ではPython GIL(以後、GIL)という排他ロックの仕組みが使われます。

簡単にいうと、
複数のスレッドの中でGILを取得したスレッドのみが処理を実行できるといったものです。

さらにいうと、
1つのプロセスは複数のスレッドを保持することができますが、GILは1つのプロセスにつき1つしか持つことができません。

すなわち、1つのプロセス内のスレッドたちが1つのGILを相互に共有し合っているということです。

Pythonではスレッドが実行される際にそのスレッドがGILを保有しているかをチェックします。

つまり、1つのプロセスで一度に実行できるスレッドはGILを保有しているスレッドの1つのみです。

そして、GILはスレッドが待ち時間(I/O待ち)になると開放されて別のスレッドへ受け渡されます。

これを踏まえると、Pythonにおけるマルチスレッドの仕組みは以下のようになります。

・I/O待ちになり、GILを保有しているスレッドがGILを手放す
  ↓
・手放されたGILを受け取った他のスレッドが実行される
  ↓
・実行されたスレッドがI/O待ちになり、またGILを手放す
  ↓
・手放されたGILを受け取った他のスレッドが実行される
  ↓
・以後、GILの受け渡しを繰り返す

先ほども説明しましたが、
Pythonにおいてマルチスレッドが効果を発揮するのはスレッドにおいて長い待ち時間が発生する場合です。

これはユーザーからの入力を受け付けたり、ファイルにアクセスしたりする処理などが行われている時です。

上記のような時間がかかる操作を待つ間にGILを一旦開放し、
他のスレッドを動作させ、時間がかかる操作が終了した時点でGILを返して処理を再開することで、
シングルスレッドでは長く待つ必要があった時間を有効活用することができます。

一方、待ち時間がほぼ発生しないような数値計算などの処理の場合、
マルチスレッドで処理を実行してもGILの受け渡しに余分な労力と時間が使われるため、
かえって全体の処理速度が落ちてしまいます。

※Python GILはCPythonにのみ存在する話

前提として、プログラミング言語には処理系というものがあります。
処理系とはプログラミング言語で記述されたプログラムを実行するためのソフトウェアのことです。
つまり、処理系によってプログラミング言語で記述されたコードを実行することができるのです。
そして、Pythonの処理系にはいくつか種類がありますが、
最も有名で一般的に使われている処理系がCPythonになります。
そして、Python GILはCPythonにおける仕様であり、
他のPython処理系(JythonやIronPythonなど)には存在しません。
ではなぜ不便なPython GILがあるCPythonを使っているかと言うと、
詳細は省略しますが、機能面などでCPythonを使うメリットが大きいためです。
したがって、単にPythonというとCPythonを指すため、
Pythonでマルチスレッドを使っていく上ではPython GILの考慮が必須になるのです。

Pythonにおけるマルチプロセスとマルチスレッドを扱うライブラリ

Pythonにおいてはマルチプロセスを扱うライブラリとして、multiprocessingがあり、マルチスレッドだとthreadingがあります。

しかし、Python3.2以降ではmultiprocessingとthreadingを2つにまとめたconcurrent.futuresが誕生しました。

なので、Pythonでマルチプロセスとマルチスレッドを扱う場合はconcurrent.futuresを使うのが一般的でしょう。

ちなみに、上記3つのライブラリは標準ライブラリであるため、インストールは不要です。

Pythonで並行処理を実装する

それでは実際にPythonで並行処理を実装していきましょう。

コードは以下の通りです。

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time


def func_1():
    for i in range(2):
        time.sleep(3)
        print('func_1:{}回目'.format(i+1))


def func_2(bro_1, bro_2):
    for i in range(2):
        time.sleep(2)
        print('func_2:{}回目 兄:{} 弟:{}'.format(i+1, bro_1, bro_2))


if __name__ == '__main__':
    print('開始')
    with ThreadPoolExecutor() as executor:
        executor.submit(func_1)
        executor.submit(func_2, 'マリオ', 'ルイージ')# 関数に引数がある場合は左記のように渡す
    print('終了')

実行結果:
開始
func_2:1回目 兄:マリオ 弟:ルイージ
func_1:1回目
func_2:2回目 兄:マリオ 弟:ルイージ
func_1:2回目
終了

実行結果を見ると、func_1()とfunc_2()が並行に実行されていることが確認できます。



また、関数に戻り値がある場合は以下のように受け取ります。

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time


def func_1():
    for i in range(2):
        time.sleep(3)
        print('func_1:{}回目'.format(i+1))


def func_2(bro_1, bro_2):
    for i in range(2):
        time.sleep(2)
        print('func_2:{}回目 兄:{} 弟:{}'.format(i+1, bro_1, bro_2))
    return 'func_2の戻り値'


if __name__ == '__main__':
    print('開始')
    with ThreadPoolExecutor() as executor:
        executor.submit(func_1)
        func_2_result = executor.submit(func_2, 'マリオ', 'ルイージ')# 関数に引数がある場合は左記のように渡す
    print(func_2_result.result())# 戻り値をresult()で取得して受け取る
    print('終了')

実行結果:
開始
func_2:1回目 兄:マリオ 弟:ルイージ
func_1:1回目
func_2:2回目 兄:マリオ 弟:ルイージ
func_1:2回目
func_2の戻り値
終了

さらに、ThreadPoolExecutor()には引数としてmax_workersを指定できます。

max_workersでは扱うスレッドの数を指定します。

例えば、max_workersに2を指定して3つの処理を実行する場合、
2つのスレッドで3つの処理を並行処理で捌くということです。

with ThreadPoolExecutor(max_workers=2) as executor:
※前後省略

ちなみに、max_workersを指定しない場合は実行するCPUの性能によってスレッド数が決まります。

Pythonで並列処理を実装する

次に並列処理を実装します。

コードについては並行処理で記述したThreadPoolExecutorをProcessPoolExecutorへ変えるだけです。

from concurrent.futures import ProcessPoolExecutor
import time


def func_1():
    for i in range(2):
        time.sleep(3)
        print('func_1:{}回目'.format(i+1))


def func_2(bro_1, bro_2):
    for i in range(2):
        time.sleep(2)
        print('func_2:{}回目 兄:{} 弟:{}'.format(i+1, bro_1, bro_2))


if __name__ == '__main__':
    print('開始')
    with ProcessPoolExecutor() as executor:
        executor.submit(func_1)
        executor.submit(func_2, 'マリオ', 'ルイージ')# 関数に引数がある場合は左記のように渡す
    print('終了')

実行結果:
開始
func_2:1回目 兄:マリオ 弟:ルイージ
func_1:1回目
func_2:2回目 兄:マリオ 弟:ルイージ
func_1:2回目
終了

なお、ProcessPoolExecutor()についても引数としてmax_workersを指定できます。

ProcessPoolExecutor()におけるmax_workersはプロセッサの数の指定です。

with ProcessPoolExecutor(max_workers=2) as executor:
※前後省略

まとめ

以上がPythonで並列処理と並行処理を実装する方法になります。

Pythonにおける並列処理と並行処理の実装についてご理解いただけたでしょうか。

長々と説明してきましたが、最低限以下のことを押さえていただければOKでしょう。

○Pythonで並行処理を実装する
・マルチスレッドを使う
・concurrent.futuresのThreadPoolExecutorを使う
・I/Oバウンドな処理に向いている
→例)ファイルの読み書き、Web APIの利用、ネットワーク通信

○Pythonで並列処理を実装する
・マルチプロセスを使う
・concurrent.futuresのProcessPoolExecutorを使う
・CPUバウンドな処理に向いている
→例)複雑or大量の数値計算

必読

フリーランスエンジニアが案件獲得方法とは?自ら営業せずに案件を獲得するには?実務経験1年未満でも大丈夫なの? 本記事ではこのような疑問を解決します。これからフリーランスエンジニアとして独立したい方は、兎にも角にも案件の獲得が急務です[…]

アイキャッチ画像