コンテンツへスキップ

並行処理とasync/await

パス操作関数async def構文に関する詳細と、非同期コード、並行処理、並列処理に関する背景について。

お急ぎですか?

まとめ

次のように、awaitで呼び出すように指示されているサードパーティライブラリを使用している場合

results = await some_library()

その場合、パス操作関数を次のようにasync defで宣言します。

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

Note

awaitは、async defで作成された関数内でのみ使用できます。


サードパーティライブラリが何らかの通信(データベース、API、ファイルシステムなど)を行い、awaitの使用をサポートしていない場合(これは現在、ほとんどのデータベースライブラリに当てはまります)、パス操作関数を通常どおりdefのみで次のように宣言します。

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

アプリケーションが(何らかの理由で)他のものと通信して応答を待つ必要がない場合でも、awaitを内部で使用する必要がなくても、async defを使用してください。


不明な場合は、通常のdefを使用してください。


: パス操作関数では、必要に応じてdefasync defを混在させ、それぞれに最適なオプションを使用して定義できます。FastAPIはそれらを適切に処理します。

いずれにしても、上記のどのケースでも、FastAPIは非同期で動作し、非常に高速です。

しかし、上記の手順に従うことで、パフォーマンス最適化を行うことができます。

技術的な詳細

Pythonの最新バージョンは、asyncawait構文を使用して、"コルーチン"と呼ばれるものを使った"非同期コード"をサポートしています。

以下のセクションでそのフレーズを部分的に見ていきましょう。

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

非同期コード

非同期コードとは、言語💬がコンピュータ/プログラム🤖に、コードのある時点で、何か別のものがどこか別の場所で完了するのを待つ必要があることを伝える方法がある、ということを意味します。その何か別のものを"slow-file"📝と呼びましょう。

したがって、その間、コンピュータは"slow-file"📝が完了するのを待つ間に、他の作業を行うことができます。

その後、コンピュータ/プログラム🤖は、再度待機しているため、またはその時点で持っていたすべての作業を完了したときに、いつでも戻ってきます。そして、待機していたタスクのいずれかがすでに完了しているかどうかを確認し、行うべきことを行います。

次に、🤖は最初に完了したタスク(例えば、私たちの"slow-file"📝)を取り、それに対して行うべきことを続けます。

その「何か別のものを待つ」とは、通常、比較的「遅い」(プロセッサとRAMメモリの速度と比較して)I/O操作を指します。例えば、以下のような待機です。

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

実行時間のほとんどがI/O操作の待機に費やされるため、これらを「I/Oバウンド」操作と呼びます。

「非同期」と呼ばれるのは、コンピュータ/プログラムが遅いタスクと「同期」する必要がなく、タスクの結果を受け取って作業を続けるために、タスクが完了する正確な瞬間を何もしないで待つ必要がないからです。

そうではなく、「非同期」システムであるため、タスクが完了すると、コンピュータ/プログラムが何らかの作業を終えるのを少し(数マイクロ秒)待ってから、結果を受け取り、それらを使って作業を続けることができます。

「同期」(「非同期」の反対)の場合、コンピュータ/プログラムは、待機を伴うステップであっても、別のタスクに切り替える前にすべてのステップを順序どおりに実行するため、「順次」という用語も一般的に使用されます。

並行処理とハンバーガー

上記で説明した非同期コードのアイデアは、「並行処理」とも呼ばれます。これは「並列処理」とは異なります。

並行処理並列処理はどちらも「多かれ少なかれ同時に起こる異なること」に関係しています。

しかし、並行処理並列処理の間にはかなり大きな違いがあります。

その違いを見るために、ハンバーガーに関する以下の話を想像してみてください。

並行ハンバーガー

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

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

レジ係はキッチンにいる料理人に何かを伝え、彼らはあなたのハンバーガーを準備する必要があることを知ります(たとえ彼らが現在前の顧客のために準備している最中であっても)。

あなたは支払いをします。💸

レジ係はあなたの順番の番号を渡します。

待っている間、あなたは好きな人と一緒にテーブルを選び、座って長い間話します(あなたのハンバーガーはとても豪華なので、準備に時間がかかります)。

好きな人とテーブルに座ってハンバーガーを待っている間、あなたは好きな人がどれほど素晴らしい、かわいくて賢いかを感嘆して時間を過ごすことができます✨😍✨。

待っている間、好きな人と話しながら、時々カウンターに表示されている番号を確認し、自分の番かどうかを見ます。

そして、ある時点で、ついにあなたの番が来ます。あなたはカウンターに行き、ハンバーガーを受け取ってテーブルに戻ります。

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

情報

Ketrina Thompsonによる美しいイラスト。🎨


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

列に並んでいる間、あなたはただアイドル状態😴で、自分の番を待っているだけで、あまり「生産的」なことはしていません。しかし、レジ係は注文を取っているだけなので(準備はしていません)、列は早く進みます。だから問題ありません。

そして、あなたの番が来たら、あなたは実際に「生産的」な作業をします。メニューを処理し、何が欲しいか決め、好きな人の選択肢を得て、支払い、正しいお札やカードを渡したか確認し、正しく請求されているか確認し、注文に正しい品目が含まれているか確認します。

しかし、ハンバーガーがまだ手元にないにもかかわらず、あなたのレジ係との作業は「一時停止」⏸しています。なぜなら、ハンバーガーができるのを待つ🕙必要があるからです。

しかし、カウンターを離れて、番号のついたテーブルに座ると、あなたは好きな人に注意を切り替え🔀て、そのことに「取り組む」⏯ 🤓ことができます。そうすれば、あなたは再び、好きな人とイチャイチャする😍という、非常に「生産的」なことをしています。

その後、レジ係💁は、カウンターのディスプレイにあなたの番号を表示することで「ハンバーガーはできました」と言いますが、表示された番号があなたの順番に変わっても、あなたはすぐに狂ったように飛び跳ねたりしません。あなたは自分の順番の番号を持っているので誰もあなたのハンバーガーを盗まないことを知っていますし、彼らも自分たちの番号を持っています。

だから、あなたは好きな人が話を終えるのを待ち(現在の作業⏯/処理中のタスク🤓を終え)、優しく微笑んで、ハンバーガーを取りに行くことを伝えます⏸。

それからあなたはカウンター🔀に行き、完了した最初のタスク⏯に取り掛かり、ハンバーガーを取り、ありがとうと言ってテーブルに持っていきます。これで、カウンターとのインタラクションのそのステップ/タスク⏹が終了します。これにより、「ハンバーガーを食べる」という新しいタスク🔀 ⏯が作成されますが、「ハンバーガーを受け取る」という前のタスクは終了⏹しています。

並列ハンバーガー

次に、これらが「並行ハンバーガー」ではなく、「並列ハンバーガー」であると想像してみましょう。

あなたは好きな人と一緒に並列ファストフード店に行きます。

何人か(例えば8人)のレジ係が同時に料理人でもあり、あなたの前の人たちの注文を受けている間、あなたは列に並びます。

8人のレジ係のそれぞれが、次の注文を受ける前にすぐにハンバーガーを準備するので、あなたの前の全員がカウンターを離れる前にハンバーガーができるのを待っています。

そしてついにあなたの番が来ました。あなたは好きな人と自分のために2つの豪華なハンバーガーを注文します。

あなたは支払いをします💸。

レジ係はキッチンに行きます。

あなたはカウンターの前で🕙待っています。順番の番号がないので、誰もあなたのハンバーガーを横取りしないようにするためです。

あなたと好きな人は、誰にも前に割り込まれたり、ハンバーガーが到着したときに取られたりしないように忙しくしているので、好きな人に注意を払うことができません。😞

これは「同期」作業です。あなたはレジ係/料理人👨‍🍳と「同期」しています。カウンターの前にいて🕙、レジ係/料理人👨‍🍳がハンバーガーを作り終えてあなたに渡す正確な瞬間を待つ必要があります。さもないと、誰かがそれらを持って行ってしまうかもしれません。

その後、あなたのレジ係/料理人👨‍🍳は、カウンターの前で長い間待たされた後🕙、ついにあなたのハンバーガーを持って戻ってきます。

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

あなたはただそれらを食べ、それで終わりです。⏹

ほとんどの時間をカウンターの前で待つ🕙のに費やしたため、あまり会話やイチャイチャはありませんでした。😞

情報

Ketrina Thompsonによる美しいイラスト。🎨


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

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

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


これは、ハンバーガーの並列版の物語になるでしょう。🍔

これのより「現実の」例を挙げるなら、銀行を想像してください。

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

すべてのレジ係が、次々と顧客とすべての作業を行っていました👨‍💼⏯。

そしてあなたは長い時間🕙列で待つ必要があり、さもなければ順番を失います。

おそらく、好きな人😍を銀行🏦での用事に連れて行きたくないでしょう。

ハンバーガーのまとめ

「好きな人とのファストフードハンバーガー」というこのシナリオでは、多くの待機🕙があるため、並行システム⏸🔀⏯を持つ方がはるかに理にかなっています。

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

非常に多くのユーザーがいますが、サーバーは彼らのあまり良くない接続がリクエストを送信するのを待っています🕙。

そして、応答が返ってくるのを再び待っています🕙。

この「待機」🕙はマイクロ秒単位で測定されますが、それでもすべて合計すると、最終的にはかなりの待機時間になります。

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

この種の非同期性は、NodeJSを人気にした理由であり(NodeJSは並列ではないにもかかわらず)、Goがプログラミング言語として強い理由でもあります。

そして、それと同じレベルのパフォーマンスをFastAPIで得ることができます。

そして、並列性と非同期性を同時に持つことができるため、ほとんどのテスト済みNodeJSフレームワークよりも高いパフォーマンスと、Cに近いコンパイル言語であるGoに匹敵するパフォーマンスを得ることができます(すべてStarletteのおかげです)

並行処理は並列処理より優れていますか?

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

並行処理は並列処理とは異なります。そして、多くの待機を伴う特定のシナリオでより優れています。そのため、Webアプリケーション開発では並列処理よりもはるかに優れていることが一般的です。しかし、すべてに当てはまるわけではありません。

そこで、バランスを取るために、以下の短い話を想像してみてください。

あなたは大きくて汚い家を掃除しなければなりません。

はい、それがすべてです。.


どこにも待機🕙はなく、家の複数の場所でやるべき作業がたくさんあるだけです。

ハンバーガーの例のように、最初にリビングルーム、次にキッチンといった順番を設けることもできますが、何も待機🕙していないので、ただ掃除して掃除するだけなので、順番は何も影響しません。

順番(並行処理)があってもなくても、完了にかかる時間は同じで、同じ量の作業をすることになります。

しかしこの場合、元レジ係/料理人/今は清掃員である8人を連れてきて、彼らそれぞれ(とあなた)が家の一区画を担当して掃除できれば、追加の助けを借りてすべての作業を並列で行い、はるかに早く終わらせることができます。

このシナリオでは、各清掃員(あなたを含む)がプロセッサとなり、それぞれの仕事の分担を行います。

そして、ほとんどの実行時間が実際の作業(待機ではなく)に費やされ、コンピュータでの作業はCPUによって行われるため、これらの問題は「CPUバウンド」と呼ばれます。


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

例えば

  • 音声または画像処理
  • コンピュータービジョン: 画像は数百万のピクセルで構成され、各ピクセルには3つの値/色があり、通常、それらのピクセルに対して同時に何かを計算する必要があります。
  • 機械学習: 通常、多くの「行列」と「ベクトル」の乗算が必要です。巨大なスプレッドシートに数字が並んでいて、それらすべてを同時に乗算するようなものだと考えてください。
  • ディープラーニング: これは機械学習のサブフィールドなので、同じことが当てはまります。ただ、乗算する単一のスプレッドシートの数字だけでなく、巨大なセットがあり、多くの場合、それらのモデルを構築および/または使用するために特別なプロセッサを使用します。

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

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

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

それに加えて、Pythonがデータサイエンス、機械学習、特にディープラーニングの主要言語であるという単純な事実が、FastAPIをデータサイエンス/機械学習のWeb APIおよびアプリケーション(その他多数)に非常に適したものにしています。

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

asyncawait

Pythonの最新バージョンでは、非同期コードを定義するための非常に直感的な方法があります。これにより、通常の「順次」コードのように見え、適切なタイミングで「待機」が行われます。

結果を返す前に待機が必要な操作があり、これらの新しいPython機能をサポートしている場合、次のようにコーディングできます。

burgers = await get_burgers(2)

ここでの鍵はawaitです。これはPythonに、get_burgers(2)が処理を終える🕙まで待機⏸してから、結果をburgersに格納するよう指示します。これにより、Pythonはその間に他の何か🔀 ⏯(別のリクエストの受信など)を行うことができることを認識します。

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

async def get_burgers(number: int):
    # Do some asynchronous stuff to create the burgers
    return burgers

...defの代わりに

# This is not asynchronous
def get_sequential_burgers(number: int):
    # Do some sequential stuff to create the burgers
    return burgers

async defを使用すると、Pythonは、その関数内でawait式を認識する必要があり、その関数の実行を「一時停止」⏸して、戻る前に他のこと🔀を行うことができることを認識します。

async def関数を呼び出す場合は、それを「await」する必要があります。したがって、これは機能しません。

# This won't work, because get_burgers was defined with: async def
burgers = get_burgers(2)

したがって、awaitで呼び出すことができると指示するライブラリを使用している場合は、次のように、async defを使用してそのライブラリを使用するパス操作関数を作成する必要があります。

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

より技術的な詳細

awaitasync defで定義された関数内でのみ使用できることに気づいたかもしれません。

しかし同時に、async defで定義された関数は「待機」される必要があります。したがって、async defを持つ関数は、async defで定義された関数内でのみ呼び出すことができます。

では、卵と鶏についてですが、最初のasync関数はどのように呼び出すのでしょうか?

FastAPIを使用している場合、その「最初の」関数がパス操作関数になるため、そのことについて心配する必要はありません。FastAPIは正しい処理方法を知っています。

ただし、FastAPIなしでasync / awaitを使用したい場合も可能です。

独自の非同期コードの記述

Starlette(およびFastAPI)はAnyIOをベースにしており、Pythonの標準ライブラリであるasyncioTrioの両方と互換性があります。

特に、独自のコードでより高度なパターンを必要とする高度な並行処理のユースケースには、AnyIOを直接使用できます。

また、FastAPIを使用していなかったとしても、AnyIOを使用して独自の非同期アプリケーションを記述し、高い互換性を持ち、その利点(例: 構造化並行処理)を得ることもできます。

私はAnyIOの上に、型アノテーションを少し改善し、より良い自動補完インラインエラーなどを得るための薄い層として別のライブラリを作成しました。また、独自の非同期コード理解し、記述するのに役立つ親しみやすい導入とチュートリアルも用意されています。Asyncerです。これは、非同期コードと通常の(ブロック/同期)コードを組み合わせる必要がある場合に特に役立つでしょう。

その他の非同期コードの形式

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

しかし、非同期コードの作業をはるかに容易にします。

この同じ構文(またはほぼ同一の構文)は、最近のJavaScriptの最新バージョン(ブラウザとNodeJS)にも含まれました。

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

以前のバージョンのPythonでは、スレッドやGeventを使用することもできましたが、コードははるかに理解、デバッグ、検討が複雑でした。

以前のバージョンのNodeJS/ブラウザJavaScriptでは、「コールバック」を使用していました。これは「コールバック地獄」につながります。

コルーチン

コルーチンとは、async def関数によって返されるものを指す非常に凝った用語です。Pythonはそれが関数のようなものであり、開始することができ、ある時点で終了するが、内部にawaitがあるときはいつでも一時停止⏸する可能性があることを知っています。

しかし、asyncawaitを使った非同期コードのこのすべての機能は、多くの場合「コルーチン」の使用としてまとめられます。これはGoの主要な機能である「Goroutines」に匹敵します。

結論

上記の同じフレーズをもう一度見てみましょう。

Pythonの最新バージョンは、asyncawait構文を使用して、"コルーチン"と呼ばれるものを使った"非同期コード"をサポートしています。

これでより意味が理解できたはずです。✨

これらすべてがFastAPI(Starlette経由)を動かし、印象的なパフォーマンスを実現しているのです。

非常に技術的な詳細

Warning

これはおそらくスキップしても問題ありません。

これらは、FastAPIが内部でどのように機能するかに関する非常に技術的な詳細です。

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

パス操作関数

パス操作関数async defの代わりに通常のdefで宣言すると、それは直接呼び出されるのではなく(サーバーをブロックするため)、外部のスレッドプールで実行され、その結果が待機されます。

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

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

依存性注入

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

サブ依存関係

複数の依存関係とサブ依存関係が互いに要求し合う(関数定義のパラメータとして)ことがあります。それらの一部はasync defで作成され、一部は通常のdefで作成されることがあります。それでも機能し、通常のdefで作成されたものは、「待機」されるのではなく、外部スレッド(スレッドプールから)で呼び出されます。

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

直接呼び出す他のユーティリティ関数は、通常のdefまたはasync defで作成でき、FastAPIは呼び出し方法に影響を与えません。

これは、FastAPIがあなたのために呼び出す関数、つまりパス操作関数と依存関係とは対照的です。

ユーティリティ関数がdefを持つ通常の関数の場合、スレッドプールではなく直接呼び出され(コードに記述したとおりに)、async defで関数が作成されている場合は、コードでその関数を呼び出すときにawaitする必要があります。


繰り返しになりますが、これらは非常に技術的な詳細であり、おそらくあなたがそれらを探しに来た場合に役立つでしょう。

そうでなければ、上記のセクションのガイドラインで問題ありません: お急ぎですか?