こんにちは!
仕事でfastAPIを触っているのですが、マルチスレッドとasyncioの違いが分からなかったので備忘録です。
pythonのマルチスレッド、asyncioの違いは?
マルチスレッドもasyncioのどちらも「平行」にタスクを実行する非同期処理を提供しています。
しかし、内部の仕組みは以下のように異なります。
マルチスレッド:複数スレッドを用意してタスクごとにスレッドを切り替える
asyncio:シングルスレッド内でタスクを切り替える
スレッド切り替え時のオーバーヘッドが少なくなるので、マルチスレッドに比べasyncioは効率的です。
マルチスレッドの場合は「並列処理」もできるのでは?
Cpython(C言語で構築されたPython)の場合はマルチスレッドの環境下でも、Pythonのバイトコードを実行できるのは一つのスレッドだけです。
この制約はPythonのGIL(Global Interpreter Lock)によるものです。C言語のパッケージはスレッドセーフでないものもあるので、スレッドセーフを保てないため、このような制約がついているようです。ここがGoやC++,Rustにい比べて遅いと言われている理由のようです。
なので、Pythonのマルチスレッドは、CPUバウンドなタスクでは効果を発揮できず、I/Oバウンドなタスクにおいて効果を発揮します。
CPUバウンドなタスクはマルチプロセスで行う必要があります。
CPUバウンドとI/Oバウンドとは?
CPUバウンド(CPU集約型)
処理時間がほぼ「CPUの性能」に依存する処理。例えば、1から10000までの数を順番に足していく処理などはCPUの計算処理能力が高ければ高いほど早くなる。
I/Oバウンド(I/O集約型)
処理時間がほぼ「入力と出力」に依存する処理。例えば、ファイルの読み込み、書き込みやデータベースのクエリの実行、ネットワークリクエストなど、CPUの計算能力ではなくCPUに待機時間が発生するようなタスク。
マルチスレッドの仕組み
GILで制御されているマルチスレッドの仕組みを図にすると以下のような感じになります。
スレッド1でCPUが処理待ちするような状況が発生すると、スレッド1は自分の持っているGILを解放します。
続いて解放されたGILをスレッド2が獲得することで、ほかのスレッドはタスクを実行できなくなります。
このようにGILの獲得と解放を繰り返し、I/Oバウンドな処理を行うのがPythonにおけるマルチスレッドの処理です。
asyncioの仕組み
asyncioはマルチスレッドと同じく非同期処理を提供しますが、シングルスレッドで行われます。
詳しい仕組みを調べたのですが、公式ドキュメントを読んでも内部の詳しい仕組みまではわかりませんでした。
しかし大体の仕組みはイベントループというプロセス(?)がずっと起動していて、各asyncioタスク(コルーチン)を管理しているようです。待機中のタスクはキュー等のデータ構造に保存され、I/Oイベント発生時にタスクがキューに保存、I/Oイベント終了時に再びキューから取り出されて続きの処理を行うようです。
スレッドが一つになったので、そのスレッドで処理待ちタスクを持っておくのはなく、別のデータ構造で持っておくということみたいです。
まとめ
GO言語を学習していた時に、並列処理、並行処理を学んだのですが、スレッドの部分までは勉強できてませんでした。
今回pythonのフレームワークであるFastAPIを使用する際に、マルチプロセス、マルチスレッドと言った単語がでてきてasyncioとの違いが気になったので詳しく調べました。
I/Oバウンドなタスクに関してはasyncioがすごく便利だということが分かりました。
また、CPUバウンドなタスクについてはマルチプロセスやGoの並列処理機能を使えばよいですが、スレッドセーフになるように気を付けようと思いました。