Les variables globales sont-elles thread-safe dans flask? Comment partager des données entre les demandes?

96

Dans mon application, l'état d'un objet commun est modifié en faisant des demandes, et la réponse dépend de l'état.

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')

Si je l'exécute sur mon serveur de développement, je m'attends à obtenir 1, 2, 3 et ainsi de suite. Si les demandes proviennent de 100 clients différents simultanément, un problème peut-il se produire? Le résultat attendu serait que les 100 clients différents voient chacun un numéro unique de 1 à 100. Ou quelque chose comme ceci se produira:

  1. Requêtes du client 1. self.paramest incrémenté de 1.
  2. Avant que l'instruction return puisse être exécutée, le thread bascule vers le client 2. self.paramest à nouveau incrémenté.
  3. Le thread revient au client 1 et le client reçoit le numéro 2, par exemple.
  4. Maintenant, le thread passe au client 2 et lui renvoie le numéro 3.

Puisqu'il n'y avait que deux clients, les résultats attendus étaient 1 et 2, pas 2 et 3. Un nombre a été omis.

Cela se produira-t-il réellement au fur et à mesure que j'améliore mon application? Quelles alternatives à une variable globale dois-je envisager?

Sayantankhan
la source

Réponses:

91

Vous ne pouvez pas utiliser de variables globales pour contenir ce type de données. Non seulement il n'est pas sûr pour les threads, il n'est pas sûr pour les processus , et les serveurs WSGI en production génèrent plusieurs processus. Non seulement vos décomptes seraient erronés si vous utilisiez des threads pour gérer les demandes, mais ils varieraient également en fonction du processus traité la demande.

Utilisez une source de données en dehors de Flask pour contenir des données globales. Une base de données, memcached ou redis sont toutes des zones de stockage séparées appropriées, en fonction de vos besoins. Si vous devez charger et accéder aux données Python, pensez à multiprocessing.Manager. Vous pouvez également utiliser la session pour des données simples par utilisateur.


Le serveur de développement peut fonctionner dans un seul thread et processus. Vous ne verrez pas le comportement que vous décrivez car chaque requête sera traitée de manière synchrone. Activez les threads ou les processus et vous le verrez. app.run(threaded=True)ou app.run(processes=10). (Dans la version 1.0, le serveur est threadé par défaut.)


Certains serveurs WSGI peuvent prendre en charge gevent ou un autre travailleur asynchrone. Les variables globales ne sont toujours pas thread-safe car il n'y a toujours pas de protection contre la plupart des conditions de concurrence. Vous pouvez toujours avoir un scénario dans lequel un travailleur obtient une valeur, cède, un autre la modifie, cède, puis le premier travailleur la modifie également.


Si vous avez besoin de stocker des données globales lors d' une requête, vous pouvez utiliser l' gobjet Flask . Un autre cas courant est un objet de niveau supérieur qui gère les connexions à la base de données. La distinction pour ce type de "global" est qu'il est unique à chaque requête, non utilisé entre les requêtes, et qu'il y a quelque chose qui gère la configuration et le démontage de la ressource.

davidisme
la source
25

Ce n'est pas vraiment une réponse à la sécurité des threads des globaux.

Mais je pense qu'il est important de mentionner les sessions ici. Vous recherchez un moyen de stocker des données spécifiques au client. Chaque connexion doit avoir accès à son propre pool de données, de manière threadsafe.

Ceci est possible avec les sessions côté serveur, et elles sont disponibles dans un plugin flask très soigné: https://pythonhosted.org/Flask-Session/

Si vous configurez des sessions, une sessionvariable est disponible dans toutes vos routes et elle se comporte comme un dictionnaire. Les données stockées dans ce dictionnaire sont individuelles pour chaque client qui se connecte.

Voici une courte démo:

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()

Après pip install Flask-Session, vous devriez pouvoir l'exécuter. Essayez d'y accéder à partir de différents navigateurs, vous verrez que le compteur n'est pas partagé entre eux.

lhk
la source
2

Tout en acceptant totalement les réponses positives ci-dessus et en décourageant l'utilisation des globaux pour la production et le stockage évolutif de flask, à des fins de prototypage ou de serveurs vraiment simples, fonctionnant sous le `` serveur de développement '' de flask ...

... les types de données intégrés python, et j'ai personnellement utilisé et testé le global dict, selon la documentation python ( https://docs.python.org/3/glossary.html#term-global-interpreter-lock ) sont thread safe. Pas de processus sûr.

Les insertions, recherches et lectures à partir d'un tel dict (global du serveur) seront correctes à partir de chaque session de flacon (éventuellement simultanée) exécutée sous le serveur de développement.

Lorsqu'un tel dict global est saisi avec une clé de session flask unique, il peut être plutôt utile pour le stockage côté serveur de données spécifiques à la session, sinon ne s'intégrant pas dans le cookie (taille maximale 4k).

Bien sûr, un tel dict global de serveur doit être soigneusement gardé pour devenir trop volumineux, être en mémoire. Une sorte d'expiration des «anciennes» paires clé / valeur peut être codée pendant le traitement de la demande.

Encore une fois, non recommandé pour les déploiements en production ou évolutifs, mais peut-être correct pour les serveurs locaux orientés tâches où la base de données séparée est trop importante pour la tâche donnée

...

R. Simac
la source