非同期およびノンブロッキング I/O

リアルタイムウェブ機能では、ユーザーごとに長時間アイドル状態の接続が必要です。従来の同期型ウェブサーバーでは、これはユーザーごとに1つのスレッドを割り当てることを意味し、非常にコストがかかる可能性があります。

同時接続のコストを最小限に抑えるために、Tornadoはシングルスレッドのイベントループを使用します。これは、一度に1つの操作しかアクティブにできないため、すべてのアプリケーションコードが非同期かつノンブロッキングであることを目指す必要があることを意味します。

非同期とノンブロッキングという用語は密接に関連しており、しばしば同じ意味で使用されますが、厳密には同じものではありません。

ブロッキング

関数が何か起こるのを待ってから戻る場合、関数はブロッキングします。関数は、ネットワークI/O、ディスクI/O、ミューテックスなど、さまざまな理由でブロッキングする可能性があります。実際、すべての関数は、少なくとも少しの間、実行中およびCPUを使用している間はブロッキングします(CPUブロッキングが他の種類のブロッキングと同様に真剣に受け止められるべき理由を示す極端な例として、bcryptのようなパスワードハッシュ関数を考えてみてください。これは、設計上、一般的なネットワークまたはディスクアクセスよりもはるかに長い数百ミリ秒のCPU時間を使用します)。

関数は、ある点ではブロッキングであり、別の点ではノンブロッキングである可能性があります。Tornadoのコンテキストでは、通常、ネットワークI/Oのコンテキストでのブロッキングについて話しますが、あらゆる種類のブロッキングを最小限に抑える必要があります。

非同期

非同期関数は完了する前に戻り、一般的に、アプリケーションで将来のアクションをトリガーする前にバックグラウンドで何らかの作業を実行します(通常の同期関数とは対照的に、同期関数は戻る前に実行するすべてを実行します)。非同期インターフェースにはさまざまなスタイルがあります。

  • コールバック引数

  • プレースホルダーを返す(Future, Promise, Deferred)

  • キューに配信

  • コールバックレジストリ(例:POSIXシグナル)

どのタイプのインターフェースを使用する場合でも、非同期関数は、定義上、呼び出し元との対話方法が異なります。呼び出し元に対して透過的な方法で同期関数を非同期にするための無料の方法はありません(geventのようなシステムは、非同期システムに匹敵するパフォーマンスを提供するために軽量スレッドを使用しますが、実際には非同期にはしません)。

Tornadoの非同期操作は、通常、コールバックを使用するIOLoopのような一部の低レベルコンポーネントを除いて、プレースホルダーオブジェクト(Futures)を返します。Futuresは通常、awaitまたはyieldキーワードを使用して結果に変換されます。

ここに、サンプルの同期関数を示します。

from tornado.httpclient import HTTPClient

def synchronous_fetch(url):
    http_client = HTTPClient()
    response = http_client.fetch(url)
    return response.body

そして、同じ関数をネイティブコルーチンとして非同期に書き直したものを次に示します。

from tornado.httpclient import AsyncHTTPClient

async def asynchronous_fetch(url):
    http_client = AsyncHTTPClient()
    response = await http_client.fetch(url)
    return response.body

または、古いバージョンのPythonとの互換性のために、tornado.genモジュールを使用します。

from tornado.httpclient import AsyncHTTPClient
from tornado import gen

@gen.coroutine
def async_fetch_gen(url):
    http_client = AsyncHTTPClient()
    response = yield http_client.fetch(url)
    raise gen.Return(response.body)

コルーチンは少し魔法のようですが、内部的には次のようなことを行っています。

from tornado.concurrent import Future

def async_fetch_manual(url):
    http_client = AsyncHTTPClient()
    my_future = Future()
    fetch_future = http_client.fetch(url)
    def on_fetch(f):
        my_future.set_result(f.result().body)
    fetch_future.add_done_callback(on_fetch)
    return my_future

コルーチンがフェッチが完了する前にFutureを返していることに注意してください。これがコルーチンを非同期にするものです。

コルーチンでできることはすべて、コールバックオブジェクトを渡すことでもできますが、コルーチンは、同期であるかのようにコードを整理できるため、重要な単純化を提供します。これは特にエラー処理で重要です。コルーチンではtry/exceptブロックが期待どおりに機能しますが、コールバックではこれを実現することが困難であるためです。コルーチンについては、このガイドの次のセクションで詳しく説明します。