コンテンツへスキップ

同時実行と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

もし、あなたのアプリケーションが(何らかの理由で)他のものと通信したり、応答を待たなくても良い場合は、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人)しかありませんでした。

しかし、それでも、最終的な経験は最善ではありません。😞


これはハンバーガーの並列処理に相当する物語です。🍔

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

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

すべてのレジ係が1人の顧客の後にもう1人の顧客と作業を行います👨‍💼⏯。

そして、あなたは長い時間列に並んで待たなければならず🕙、そうでなければ順番を失います。

あなたは、好きな人😍を連れて銀行🏦へ用事を済ませに行きたくないでしょう。

ハンバーガーの結論

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

これは、ほとんどのウェブアプリケーションの場合です。

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

そして、レスポンスが戻ってくるのを再び待っています🕙。

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

そのため、ウェブAPIに非同期⏸🔀⏯コードを使用することは非常に理にかなっています。

このような非同期性によって、NodeJSは人気を博しました(NodeJSは並列処理ではないにもかかわらず)、そしてそれはGoプログラミング言語の長所です。

そして、それは**FastAPI**で得られるパフォーマンスと同じレベルです。

そして、並列処理と非同期性を同時に使用できるため、テストされたほとんどのNodeJSフレームワークよりも高いパフォーマンスを実現し、Cに近いコンパイル言語であるGoと同等の性能を実現します(すべてStarletteのおかげです)

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

いいえ!それは物語の教訓ではありません。

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

そのため、バランスを取るために、次の短い物語を考えてみましょう。

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

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


どこにも待機時間🕙はありません。家の複数の場所でやるべきことがたくさんあるだけです。

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

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

しかし、この場合、8人の元レジ係/料理人/現在の掃除人を連れてきて、彼らそれぞれ(あなたも含めて)が家のゾーンを担当して掃除できれば、追加の助けを得て**並列**にすべての作業を行い、はるかに早く完了することができます。

このシナリオでは、掃除人それぞれ(あなたも含めて)がプロセッサとなり、自分の仕事を行います。

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


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

例えば

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

並行処理 + 並列処理:ウェブ + 機械学習

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

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

それに加えて、Pythonが**データサイエンス**、機械学習、特に深層学習の主要言語であるという単純な事実から、FastAPIはデータサイエンス/機械学習のウェブAPIやアプリケーション(その他多数)に非常に適しています。

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

asyncawait

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

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

burgers = await get_burgers(2)

ここで重要なのはawaitです。これはPythonに対して、burgersに結果を格納する前に、get_burgers(2)が処理を完了するまで待機する必要があることを伝えます⏸️。これにより、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

より技術的な詳細

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

しかし同時に、async defで定義された関数は「await」する必要があります。そのため、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で宣言すると、直接呼び出されるのではなく(サーバーをブロックするため)、外部スレッドプールで実行され、その後awaitされます。

上記のように動作しない別の非同期フレームワークから移行してきて、わずかなパフォーマンス向上(約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する必要があります。


繰り返しますが、これらは、検索してきた場合に役立つ可能性のある非常に技術的な詳細です。

それ以外の場合は、上記のセクションのガイドラインに従うと良いでしょう。急いでいますか?.