コンテンツにスキップ

並行処理と async / await

🌐 AI と人間による翻訳

この翻訳は、人間のガイドに基づいて AI によって作成されました。🤝

原文の意図を取り違えていたり、不自然な表現になっている可能性があります。🤖

AI LLM をより適切に誘導するのを手伝う ことで、この翻訳を改善できます。

英語版

path operation 関数のための async def 構文に関する詳細と、非同期コード、並行処理、並列処理の背景についてです。

急いでいますか?

TL;DR:

次のように await で呼び出すよう指示されているサードパーティライブラリを使っているなら:

results = await some_library()

path operation 関数は次のように async def で宣言します:

@app.get('/')
async def read_results():
    results = await some_library()
    return results

備考

awaitasync def で作られた関数の内部でしか使えません。


データベース、API、ファイルシステムなどと通信しつつ await の使用をサポートしていないサードパーティライブラリ (現在のところ多くのデータベースライブラリが該当します) を使っている場合、path operation 関数は通常どおり def で宣言してください:

@app.get('/')
def results():
    results = some_library()
    return results

アプリケーションが (何らかの理由で) ほかの何とも通信せず応答を待つ必要がないなら、await を内部で使わなくても async def を使ってください。


よく分からない場合は、通常の def を使ってください。


備考: 必要に応じて path operation 関数 では defasync def を混在させ、それぞれに最適な選択肢で定義できます。FastAPI は適切に処理します。

いずれの場合でも、FastAPI は非同期で動作し非常に高速です。

ただし上記の手順に従うことで、さらにパフォーマンス最適化が可能になります。

技術詳細

モダンな Python は 「非同期コード」「コルーチン」 と呼ばれる仕組みでサポートしており、構文は asyncawait です。

以下のセクションで、このフレーズをパーツごとに見ていきます:

  • 非同期コード
  • asyncawait
  • コルーチン

非同期コード

非同期コードとは、言語 💬 がコードのどこかの時点で、コンピュータ/プログラム 🤖 に「どこか別のところで終わるまで、別の何か」を待つ必要があると伝える手段を持っている、ということです。その「別の何か」を「遅いファイル」📝 と呼ぶことにしましょう。

その間、コンピュータは「遅いファイル」📝 が終わるまで、他の作業を進められます。

その後、コンピュータ/プログラム 🤖 は、また待つ機会が来たときや、その時点で抱えていた作業をすべて終えたときに戻ってきます。そして、待っていたタスクのどれかが終わっていないか確認し、必要な処理を実行します。

次に、最初に終わったタスク (たとえば「遅いファイル」📝) を取り、続きの処理を行います。

この「別の何かを待つ」は、通常 I/O 操作を指し、(プロセッサや RAM の速度に比べて) 相対的に「遅い」待機を伴います。例えば次のようなものです:

  • クライアントからネットワーク経由でデータが送られてくるのを待つ
  • プログラムが送信したデータをクライアントがネットワーク経由で受け取るのを待つ
  • ディスク上のファイル内容がシステムにより読み取られ、プログラムに渡されるのを待つ
  • プログラムがシステムに渡した内容がディスクに書き込まれるのを待つ
  • リモート API 操作
  • データベース操作の完了
  • データベースクエリが結果を返すのを待つ
  • など

実行時間の大半が I/O 操作の待ち時間に費やされるため、これらは「I/O バウンド」な操作と呼ばれます。

「非同期」と呼ばれるのは、コンピュータ/プログラムがその遅いタスクと「同期」(タスクがちょうど終わる瞬間を、何もせずに待つ) する必要がないからです。結果を受け取って処理を続けるために、空待ちする必要がありません。

代わりに「非同期」システムでは、タスクが終わったら、コンピュータ/プログラムが取りかかっている作業が終わるまで (数マイクロ秒ほど) 少し待ち、結果を受け取りに戻って処理を続けられます。

「非同期」と対になる「同期」は、「シーケンシャル」と呼ばれることもあります。待機が含まれていても、別のタスクに切り替える前にコンピュータ/プログラムが手順を順番に実行するためです。

並行処理とハンバーガー

上で説明した非同期コードの考え方は、「並行処理」 と呼ばれることもあります。これは 「並列処理」 とは異なります。

並行処理並列処理 も、「複数のことがだいたい同時に起きる」ことに関係します。

ただし、並行処理並列処理 の詳細はかなり異なります。

違いを見るために、ハンバーガーに関する次の物語を想像してみてください。

並行ハンバーガー

あなたは好きな人とファストフードを買いに行き、前の人たちの注文をレジ係が受ける間、列に並びます。😍

やがてあなたの番になり、好きな人と自分のために、とても豪華なハンバーガーを2つ注文します。🍔🍔

レジ係はキッチンの料理人に、あなたのハンバーガーを用意するよう声をかけます (料理人はいま前のお客さんの分を作っています)。

支払いをします。💸

レジ係はあなたに番号札を渡します。

待っている間、好きな人とテーブルに移動して座り、(豪華なハンバーガーは時間がかかるので) しばらく話します。

テーブルで待っている間、好きな人がどれだけ素敵で、かわいくて、頭が良いかを眺めて時間を過ごせます ✨😍✨。

時々カウンターの表示を見て、自分の番号になっているか確認します。

やがてあなたの番になります。カウンターに行き、ハンバーガーを受け取り、テーブルに戻ります。

あなたと好きな人はハンバーガーを食べて、楽しい時間を過ごします。✨

情報

美しいイラストは Ketrina Thompson によるものです。🎨


この物語で、あなた自身がコンピュータ/プログラム 🤖 だと想像してみてください。

列にいる間は、何も「生産的」なことをせず、自分の番を待つだけのアイドル状態 😴 です。ただしレジ係は注文を取るだけ (作りはしない) なので列は速く進み、問題ありません。

あなたの番になると、実際に「生産的」な作業をします。メニューを見て注文を決め、好きな人の分も確認し、支払い、正しい紙幣/カードを渡したか、正しく決済されたか、注文内容が正しいかなどを確認します。

しかし、ハンバーガーはまだ出来上がっていないので、レジ係とのやり取りは「一時停止」⏸ になります。ハンバーガーができるまで待つ 🕙 必要があるからです。

ただし、番号札を持ってカウンターから離れテーブルに座れば、注意を好きな人に切り替え 🔀、「その作業」⏯ 🤓 に取り組めます。好きな人といちゃつくという、とても「生産的」🤓 なことがまたできます。

レジ係 💁 がカウンターの表示にあなたの番号を出して「ハンバーガーができました」と知らせても、あなたは表示が切り替わった瞬間に飛び跳ねたりしません。自分の番号札があり、他の人にもそれぞれ番号札があるので、ハンバーガーを盗られることはないと知っているからです。

だから、好きな人の話が終わるのを待ち (現在の作業 ⏯ / 処理中のタスクを完了し 🤓)、微笑んで「ハンバーガー取ってくるね」と言います ⏸。

それからカウンターへ行き 🔀、いま完了した初期のタスク ⏯ に戻って、ハンバーガーを受け取り、礼を言ってテーブルに持っていきます。これでカウンターとのやり取りというステップ/タスクは完了 ⏹ です。その結果として「ハンバーガーを食べる」🔀 ⏯ という新しいタスクが生まれますが、先の「ハンバーガーを受け取る」タスクは完了 ⏹ しています。

並列ハンバーガー

今度は、これが「並行ハンバーガー」ではなく「並列ハンバーガー」だと想像しましょう。

あなたは好きな人と「並列」ファストフードを買いに行きます。

複数のレジ係 (例えば 8 人) が同時に料理人でもあり、前の人たちの注文を受けています。

8 人のレジ係はそれぞれ、次の注文を取る前にすぐに調理に取りかかるため、あなたの前の人たちはカウンターを離れず、ハンバーガーができるのを待っています。

ようやくあなたの番になり、好きな人と自分のために豪華なハンバーガーを 2 つ注文します。

支払いをします 💸。

レジ係はキッチンに向かいます。

番号札がないため、他の誰かに先に取られないよう、カウンターの前で立って待ちます 🕙。

あなたと好きな人は、誰にも割り込まれずハンバーガーが来たらすぐ受け取れるよう見張っているので、好きな人に注意を向けられません。😞

これは「同期」的な作業です。レジ係/料理人 👨‍🍳 と「同期」しています。レジ係/料理人 👨‍🍳 がハンバーガーを作り終えて手渡すその瞬間に、待って 🕙 その場にいなければなりません。そうでないと他の誰かに取られるかもしれません。

長い時間 🕙 カウンター前で待った後、ようやくレジ係/料理人 👨‍🍳 がハンバーガーを持って戻ってきます。

ハンバーガーを受け取り、好きな人とテーブルに行きます。

食べて、おしまいです。⏹

ほとんどの時間をカウンター前で待つ 🕙 のに費やしたため、あまり話したり、いちゃついたりできませんでした。😞

情報

美しいイラストは Ketrina Thompson によるものです。🎨


この「並列ハンバーガー」のシナリオでは、あなたは 2 つのプロセッサ (あなたと好きな人) を持つコンピュータ/プログラム 🤖 で、どちらも長い間 🕙「カウンターでの待機」に注意 ⏯ を専念しています。

ファストフード店には 8 個のプロセッサ (レジ係/料理人) があります。一方、並行ハンバーガーの店には (レジ係 1、人、料理人 1 人の) 2 個しかなかったかもしれません。

それでも、最終的な体験は最良とは言えません。😞


これはハンバーガーにおける並列版の物語です。🍔

より「現実的な」例として、銀行を想像してみてください。

つい最近まで、ほとんどの銀行には複数の窓口係 👨‍💼👨‍💼👨‍💼👨‍💼 と長い行列 🕙🕙🕙🕙🕙🕙🕙🕙 がありました。

各窓口係が、一人ずつ、すべての作業を順番に行います 👨‍💼⏯。

そして、長時間 🕙 行列で待たなければ順番を失います。

銀行の用事 🏦 に、好きな人 😍 を連れて行きたいとは思わないでしょう。

ハンバーガーのまとめ

この「好きな人とファストフード」のシナリオでは、待ち時間 🕙 が多いため、並行システム ⏸🔀⏯ を使う方がはるかに理にかなっています。

これは、ほとんどの Web アプリケーションにも当てはまります。

とても多くのユーザーがいますが、サーバは彼らのあまり速くない回線からリクエストが届くのを待ち 🕙、

その後、レスポンスが戻ってくるのをまた待ちます 🕙。

この「待ち」🕙 はマイクロ秒単位で測られますが、すべてを合計すると、結局かなりの待ちになります。

だからこそ、Web API には非同期 ⏸🔀⏯ コードを使うのが理にかなっています。

これが、NodeJS を人気にした要因 (NodeJS 自体は並列ではありません) であり、プログラミング言語としての Go の強みでもあります。

そして、それが FastAPI で得られるパフォーマンスの水準です。

さらに、並列性と非同期性を同時に活用できるため、テストされた多くの NodeJS フレームワークより高い性能を発揮し、C に近いコンパイル言語である Go と同等の性能になります (すべて Starlette のおかげです)

並行処理は並列処理より優れている?

いいえ!それがこの話の教訓ではありません。

並行処理は並列処理とは異なります。そして多くの待ち時間を伴う特定のシナリオでは優れています。そのため、一般に Web アプリ開発では並列処理よりはるかに適しています。しかし、すべてに対して最良というわけではありません。

バランスを取るために、次の短い物語を想像してください。

大きくて汚れた家を掃除しなければならない。

はい、これで物語は全部です


どこにも待ち 🕙 はなく、家の複数箇所で大量の作業があるだけです。

ハンバーガーの例のように順番を決めて、まずリビング、次にキッチン、と進めてもよいのですが、何かを待つ 🕙 わけではなく、ひたすら掃除するだけなので、順番は何も影響しません。

順番の有無 (並行性の有無) に関係なく、終了までに同じ時間がかかり、同じ作業量をこなすことになります。

しかしこの場合、8 人の元レジ係/料理人/現清掃員を連れてきて、それぞれ (あなたも加えて) 家の別々のエリアを掃除できれば、並列 に作業でき、より早く終えられます。

このシナリオでは、各清掃員 (あなたを含む) がプロセッサであり、それぞれが自分の役割を果たします。

そして実行時間の大半は (待ちではなく) 実作業が占め、コンピュータでの作業は CPU によって行われます。これらの問題は「CPU バウンド」と呼ばれます。


CPU バウンドな操作の一般的な例は、複雑な数値処理が必要なものです。

例えば:

  • オーディオ画像処理
  • コンピュータビジョン: 画像は数百万のピクセルで構成され、各ピクセルには 3 つの値/色があり、通常、それらのピクセル上で同時に何かを計算する必要があります。
  • 機械学習: 多くの「行列」や「ベクトル」の乗算が必要になります。巨大なスプレッドシートに数字が入っていて、それらを同時にすべて掛け合わせることを想像してください。
  • ディープラーニング: 機械学習のサブフィールドなので同様です。掛け合わせる数字が 1 つのスプレッドシートではなく膨大な集合であり、多くの場合、それらのモデルを構築/利用するための特別なプロセッサを使います。

並行処理 + 並列処理: Web + 機械学習

FastAPI では、Web 開発で非常に一般的な並行処理 (NodeJS の主な魅力と同じ) を活用できます。

同時に、機械学習システムのような CPU バウンド なワークロードに対して、並列処理やマルチプロセッシング (複数プロセスの並列実行) の利点も活用できます。

さらに、Python が データサイエンス、機械学習、特にディープラーニングの主要言語であるという事実も相まって、FastAPI はデータサイエンス/機械学習の Web API やアプリケーション (ほか多数) に非常に適しています。

本番環境でこの並列性を実現する方法は、デプロイ のセクションを参照してください。

asyncawait

モダンな Python には、非同期コードをとても直感的に定義する方法があります。これにより、通常の「シーケンシャル」なコードのように書けて、適切なタイミングで「待ち」を行ってくれます。

結果を返す前に待ちが必要で、これらの新しい Python 機能をサポートしている操作がある場合、次のように書けます。

burgers = await get_burgers(2)

ここでの鍵は await です。burgers に結果を保存する前に、get_burgers(2) がやるべきことを終えるのを ⏸ 待つ 🕙 ように Python に伝えます。これにより Python は、その間に (別のリクエストを受け取るなど) ほかのことを 🔀 ⏯ できると分かります。

await が機能するには、この非同期性をサポートする関数の内部でなければなりません。そのためには async def で宣言します:

async def get_burgers(number: int):
    # ハンバーガーを作るために非同期の処理を行う
    return burgers

...def の代わりに:

# これは非同期ではない
def get_sequential_burgers(number: int):
    # ハンバーガーを作るためにシーケンシャルな処理を行う
    return burgers

async def を使うと、Python はその関数内で await 式に注意し、関数の実行を「一時停止」⏸ してほかのことをしに行き 🔀、戻ってくることができると分かります。

async def な関数を呼ぶときは「await」しなければなりません。したがって、次は動きません:

# 動きません。get_burgers は async def で定義されています
burgers = get_burgers(2)

そのため、await で呼べると謳っているライブラリを使っている場合は、それを使う path operation 関数async def で作る必要があります。例えば:

@app.get('/burgers')
async def read_burgers():
    burgers = await get_burgers(2)
    return burgers

より発展的な技術詳細

awaitasync def で定義された関数の内部でしか使えないことに気づいたかもしれません。

同時に、async def で定義された関数は「await」される必要があります。つまり、async def を持つ関数は、やはり async def で定義された関数の内部からしか呼べません。

では、ニワトリと卵の話のように、最初の async 関数はどう呼ぶのでしょうか?

FastAPI を使っている場合は心配ありません。その「最初の」関数は path operation 関数 で、FastAPI が適切に実行してくれます。

しかし、FastAPI を使わずに async / await を使いたい場合もあります。

自分で async コードを書く

Starlette (FastAPI も) は AnyIO の上に構築されており、標準ライブラリの asyncioTrio の両方に対応しています。

特に、あなた自身のコード内で、より高度なパターンを必要とする発展的な並行処理のユースケースに対して、AnyIO を直接使えます。

仮に FastAPI を使っていなくても、AnyIO で独自の async アプリケーションを書けば、高い互換性と利点 (例: 構造化並行性) を得られます。

私は AnyIO の上に薄い層として、型注釈を少し改善し、より良い補完インラインエラーなどを得るための別ライブラリも作りました。また、理解して自分で async コードを書くのに役立つフレンドリーなイントロ/チュートリアルもあります: Asyncer。特に、async コードと通常の (ブロッキング/同期) コードを組み合わせる必要がある場合に有用です。

非同期コードの他の形式

asyncawait を使うこのスタイルは、言語としては比較的新しいものです。

しかし、これにより非同期コードの取り扱いは大幅に簡単になります。

同等 (ほぼ同一) の構文が最近の JavaScript (ブラウザと NodeJS) にも導入されました。

それ以前は、非同期コードの扱いはかなり複雑で難解でした。

以前の Python ではスレッドや Gevent を使えましたが、コードの理解・デバッグ・思考がはるかに難しくなります。

以前の NodeJS / ブラウザ JavaScript では「コールバック」を使っており、「コールバック地獄」を招きました。

コルーチン

コルーチンは、async def 関数が返すものを指す、ちょっと洒落た用語です。Python はそれを、開始できていつか終了する関数のようなものとして扱いますが、内部に await があるたびに内部的に一時停止 ⏸ するかもしれないものとして認識します。

asyncawait を用いた非同期コードの機能全体は、しばしば「コルーチンを使う」と要約されます。これは Go の主要機能「Goroutines」に相当します。

まとめ

上のフレーズをもう一度見てみましょう:

モダンな Python は 「非同期コード」「コルーチン」 と呼ばれる仕組みでサポートしており、構文は asyncawait です。

今なら、より意味が分かるはずです。✨

これらすべてが (Starlette を通じて) FastAPI を支え、印象的なパフォーマンスを実現しています。

非常に発展的な技術的詳細

注意

おそらく読み飛ばしても大丈夫です。

これは FastAPI の内部動作に関する、とても技術的な詳細です。

(コルーチン、スレッド、ブロッキング等の) 技術知識があり、FastAPI が async def と通常の def をどう扱うかに興味がある場合は、読み進めてください。

Path operation 関数

path operation 関数async def ではなく通常の def で宣言した場合、(サーバをブロックしてしまうため) 直接呼び出されるのではなく、外部のスレッドプールで実行され、それを待機します。

上記とは異なる動作の別の非同期フレームワークから来ており、ほんのわずかなパフォーマンス向上 (約 100 ナノ秒) を狙って、計算のみの些細な path operation 関数 を素の def で定義することに慣れている場合、FastAPI では効果がまったく逆になる点に注意してください。これらの場合、path operation 関数 がブロッキングな I/O を行うコードを使っていない限り、async def を使った方が良いです。

それでも、どちらの状況でも、FastAPI はあなたが以前使っていたフレームワークよりも (少なくとも同等に) 高速である 可能性が高いです。

依存関係

依存関係 についても同様です。依存関係が async def ではなく標準の def 関数である場合、外部のスレッドプールで実行されます。

サブ依存関係

複数の依存関係や サブ依存関係 を (関数定義のパラメータとして) 相互に要求させられます。その一部は async def、他は通常の def で作られていても動作します。通常の def で作られたものは「await」される代わりに、外部スレッドプールからスレッド上で呼び出されます。

その他のユーティリティ関数

あなたが直接呼び出すユーティリティ関数は、通常の def でも async def でも構いません。FastAPI はその呼び出し方に影響を与えません。

これは、FastAPI があなたの代わりに呼び出す関数 (すなわち path operation 関数 と依存関係) とは対照的です。

ユーティリティ関数が def の通常関数であれば、(あなたのコードに書いたとおりに) 直接呼び出され、スレッドプールでは実行されません。関数が async def で作られている場合は、その関数を呼ぶときに await すべきです。


繰り返しになりますが、これらは非常に技術的な詳細で、該当事項を検索してここにたどり着いた場合には役立つでしょう。

それ以外の場合は、上のセクションのガイドラインに従えば十分です: 急いでいますか?