グローバル変数はフラスコ内でスレッドセーフですか?リクエスト間でデータを共有するにはどうすればよいですか?


96

私のアプリでは、リクエストを行うことで共通オブジェクトの状態が変更され、応答は状態によって異なります。

class SomeObj():
    def __init__(self, param):
        self.param = param
    def query(self):
        self.param += 1
        return self.param

global_obj = SomeObj(0)

@app.route('/')
def home():
    flash(global_obj.query())
    render_template('index.html')

開発サーバーでこれを実行すると、1、2、3などになると期待しています。100の異なるクライアントから同時にリクエストが行われた場合、何か問題が発生する可能性がありますか?予想される結果は、100の異なるクライアントがそれぞれ1から100までの一意の番号を表示することです。または、次のようなことが起こります。

  1. クライアント1が照会します。self.param1ずつ増加します。
  2. returnステートメントを実行する前に、スレッドself.paramはクライアント2に切り替わります。再びインクリメントされます。
  3. スレッドはクライアント1に戻り、クライアントには、たとえば、番号2が返されます。
  4. これでスレッドはクライアント2に移動し、クライアントに番号3を返します。

クライアントが2つしかないため、期待される結果は2と3ではなく1と2でした。数値はスキップされました。

これは、アプリケーションをスケールアップするときに実際に起こりますか?グローバル変数に代わるものは何ですか?

回答:


91

この種のデータを保持するためにグローバル変数を使用することはできません。スレッドセーフではないだけでなく、プロセスでもありませんセーフでもあり。本番環境のWSGIサーバーは複数のプロセスを生成します。リクエストを処理するためにスレッドを使用している場合、カウントは間違っているだけでなく、どのプロセスがリクエストを処理したかによっても異なります。

Flaskの外部のデータソースを使用してグローバルデータを保持します。データベース、memcached、またはredisは、ニーズに応じて、適切な個別のストレージ領域です。Pythonデータをロードしてアクセスする必要がある場合は、を検討してくださいmultiprocessing.Manager。ユーザーごとの単純なデータのセッションを使用することもできます。


開発サーバーは、単一のスレッドとプロセスで実行できます。各リクエストは同期的に処理されるため、記述した動作は表示されません。スレッドまたはプロセスを有効にすると、表示されます。app.run(threaded=True)またはapp.run(processes=10)。(1.0では、サーバーはデフォルトでスレッド化されます。)


一部のWSGIサーバーは、geventまたは別の非同期ワーカーをサポートしている場合があります。ほとんどの競合状態に対する保護がまだないため、グローバル変数はまだスレッドセーフではありません。1つのワーカーが値を取得し、yieldし、別のワーカーがそれを変更し、yieldし、最初のワーカーもそれを変更するというシナリオを維持できます。


リクエスト中にグローバルデータを保存する必要がある場合は、Flaskのgオブジェクトを使用できます。もう1つの一般的なケースは、データベース接続を管理するいくつかの最上位オブジェクトです。このタイプの「グローバル」の違いは、リクエスト間で使用されるのではなく、リクエストごとに一意であり、リソースのセットアップと破棄を管理するものがあることです。


25

これは、グローバルのスレッドセーフに対する答えではありません。

しかし、ここでセッションについて言及することが重要だと思います。クライアント固有のデータを保存する方法を探しています。すべての接続は、スレッドセーフな方法で独自のデータプールにアクセスできる必要があります。

これはサーバー側セッションで可能であり、非常にきちんとしたフラスコプラグインで利用できます:https : //pythonhosted.org/Flask-Session/

セッションを設定すると、session変数はすべてのルートで使用でき、辞書のように動作します。このディクショナリに格納されているデータは、接続しているクライアントごとに異なります。

ここに短いデモがあります:

from flask import Flask, session
from flask_session import Session

app = Flask(__name__)
# Check Configuration section for more details
SESSION_TYPE = 'filesystem'
app.config.from_object(__name__)
Session(app)

@app.route('/')
def reset():
    session["counter"]=0

    return "counter was reset"

@app.route('/inc')
def routeA():
    if not "counter" in session:
        session["counter"]=0

    session["counter"]+=1

    return "counter is {}".format(session["counter"])

@app.route('/dec')
def routeB():
    if not "counter" in session:
        session["counter"] = 0

    session["counter"] -= 1

    return "counter is {}".format(session["counter"])


if __name__ == '__main__':
    app.run()

の後でpip install Flask-Session、これを実行できるはずです。別のブラウザからアクセスしてみてください。カウンタがブラウザ間で共有されていないことがわかります。


2

上記の回答を完全に受け入れ、プロトタイピングまたは非常に単純なサーバーの目的で、フラスコの「開発サーバー」の下で実行するために、生産およびスケーラブルなフラスコの保管にグローバルを使用することを推奨しません...

... pythonの組み込みデータ型、およびdictpythonのドキュメント(https://docs.python.org/3/glossary.html#term-global-interpreter-lock)のように、私は個人的にグローバルを使用してテストしましたスレッドセーフ。安全に処理できません。

そのような(サーバーグローバル)辞書からの挿入、ルックアップ、読み取りは、開発サーバーで実行されている各フラスコセッション(同時実行の可能性があります)からは問題ありません。

このようなグローバルな辞書が一意のフラスコセッションキーでキー設定されている場合、セッション固有のデータをサーバー側で保存する場合に有効です。そうしないと、Cookieに適合しません(最大サイズ4k)。

もちろん、そのようなサーバーのグローバル辞書は、メモリ内になりすぎて大きくなりすぎないように注意深く保護する必要があります。ある種の期限切れの「古い」キー/値のペアは、リクエスト処理中にコード化できます。

繰り返しますが、本番環境またはスケーラブルなデプロイメントには推奨されませんが、特定のタスクに対して個別のdbが多すぎるローカルタスク指向のサーバーではおそらく問題ありません。

...

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.