コンテンツへスキップ

並行処理とasync / await

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

お急ぎですか?

要約

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

results = await some_library()

その場合、以下のように、パス操作関数async defで宣言してください。

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

注意

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
  • コルーチン

非同期コード

非同期コードとは、言語💬がコンピュータ/プログラム🤖に、コードのある時点で何か別のものがどこか別の場所で終了するのを待たなければならないと伝える方法があることを意味します。その何か別のものを「スローファイル」📝としましょう。

つまり、その間、コンピュータは「スローファイル」📝が終了するのを待っている間に、他の作業を行うことができます。

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

次に、🤖は最初に終了したタスク(例えば「スローファイル」📝)を取得し、それで行うべきだったことを続けます。

この「何か別のものを待つ」とは、通常、比較的「遅い」(プロセッサや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を介して)の原動力であり、驚くべきパフォーマンスを発揮する理由です。

非常に技術的な詳細

警告

この部分はスキップしても問題ありません。

これらはFastAPIの内部動作に関する非常に技術的な詳細です。

もしあなたがかなり技術的な知識(コルーチン、スレッド、ブロッキングなど)を持っていて、FastAPIがasync defと通常のdefをどのように処理するかについて興味があるなら、続けてください。

パス操作関数

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

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

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

依存性注入

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

サブ依存関係

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

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

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

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

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


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

それ以外の場合は、上記のセクションのガイドラインで十分です。お急ぎですか?