Flask で非同期処理を扱う(async/await の制限と回避策)

Flask 2.0 から async/await がサポートされたが、制限や落とし穴がある。Flask で非同期処理を扱う際の注意点と回避策を解説する。

Flask 2.0 の async サポート

Flask 2.0 以降では、ビュー関数を async def で定義できる。

from flask import Flask

app = Flask(__name__)

@app.route('/async')
async def async_view():
    await some_async_operation()
    return 'Done'

ただし、Flask 自体は WSGI フレームワークであり、真の非同期フレームワーク(FastAPI など)とは異なる。

制限: 同期的なコンテキスト

Flask のリクエストコンテキストは同期的に設計されている。非同期タスクを別スレッドやコルーチンで実行すると、コンテキストが伝播しない。

import asyncio

@app.route('/problem')
async def problem():
    # 危険: 別タスクではコンテキストが存在しない
    task = asyncio.create_task(background_work())
    await task
    return 'Done'

async def background_work():
    # RuntimeError: Working outside of application context
    print(current_app.name)

回避策 1: copy_current_request_context

Flask が提供するデコレータでコンテキストをコピーできる。

from flask import copy_current_request_context

@app.route('/safe')
async def safe():
    @copy_current_request_context
    async def background_work():
        # コンテキストがコピーされている
        return current_app.name
    
    result = await background_work()
    return result

回避策 2: 必要な値を先に取得

コンテキストに依存するデータを先に取り出してから非同期処理に渡す。

@app.route('/extract')
async def extract():
    # 先に必要な値を取得
    config_value = current_app.config['API_KEY']
    user_id = g.user.id
    
    # 非同期処理には値だけを渡す
    result = await process_async(config_value, user_id)
    return result

非同期 WSGI サーバー

Flask で async を最大限活用するには、非同期対応の WSGI サーバーを使う。

pip install asgiref
gunicorn -k uvicorn.workers.UvicornWorker app:app

または Hypercorn を使う。

pip install hypercorn
hypercorn app:app

Flask vs 真の非同期フレームワーク

FlaskWSGI ベース、async は後付けサポート
FastAPIASGI ネイティブ、非同期が第一級市民
QuartFlask 互換の ASGI フレームワーク

大量の I/O バウンドな処理が必要なら、Quart への移行も検討すべきである。Quart は Flask とほぼ同じ API を持つ。

from quart import Quart

app = Quart(__name__)

@app.route('/async')
async def async_view():
    # Quart では自然に非同期が使える
    result = await async_db_query()
    return result

Flask での非同期は可能だが、制限を理解した上で使うこと。本格的な非同期が必要なら、ASGI フレームワークを検討すべきである。