Czy zmienne globalne są bezpieczne dla wątków w Flask? Jak udostępniać dane między żądaniami?

101

W mojej aplikacji stan wspólnego obiektu jest zmieniany poprzez wysyłanie żądań, a odpowiedź zależy od stanu.

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

Jeśli uruchomię to na moim serwerze deweloperskim, spodziewam się uzyskać 1, 2, 3 i tak dalej. Jeśli wnioski są składane jednocześnie od 100 różnych klientów, czy coś może pójść nie tak? Oczekiwany wynik byłby taki, że każdy ze 100 różnych klientów zobaczyłby niepowtarzalny numer od 1 do 100. A może wydarzy się coś takiego:

  1. Zapytania klienta 1. self.paramjest zwiększana o 1.
  2. Przed wykonaniem instrukcji return wątek przełącza się na klienta 2. self.paramjest ponownie zwiększany.
  3. Wątek przełącza się z powrotem na klienta 1, a klientowi zwracany jest, powiedzmy, numer 2.
  4. Teraz wątek przechodzi do klienta 2 i zwraca mu / jej numer 3.

Ponieważ było tylko dwóch klientów, oczekiwane wyniki to 1 i 2, a nie 2 i 3. Liczba została pominięta.

Czy tak się stanie, gdy zwiększę skalę mojej aplikacji? Na jakie alternatywy dla zmiennej globalnej powinienem spojrzeć?

sayantankhan
źródło

Odpowiedzi:

98

Nie możesz używać zmiennych globalnych do przechowywania tego rodzaju danych. Nie tylko nie jest bezpieczny dla wątków, ale także nie jest bezpieczny dla procesów , a serwery WSGI w środowisku produkcyjnym uruchamiają wiele procesów. Nie tylko twoje liczby byłyby błędne, gdybyś używał wątków do obsługi żądań, ale także różniłyby się w zależności od tego, który proces obsłużył żądanie.

Użyj źródła danych poza Flask, aby przechowywać dane globalne. Baza danych, memcached lub redis to odpowiednie oddzielne obszary przechowywania, w zależności od potrzeb. Jeśli chcesz załadować dane Pythona i uzyskać do nich dostęp, rozważ multiprocessing.Manager. Możesz również użyć sesji dla prostych danych dotyczących poszczególnych użytkowników.


Serwer deweloperski może działać w jednym wątku i procesie. Nie zobaczysz opisanego zachowania, ponieważ każde żądanie będzie obsługiwane synchronicznie. Włącz wątki lub procesy, a zobaczysz to. app.run(threaded=True)lub app.run(processes=10). (W wersji 1.0 serwer jest domyślnie powiązany z wątkami).


Niektóre serwery WSGI mogą obsługiwać gevent lub innego pracownika asynchronicznego. Zmienne globalne nadal nie są bezpieczne dla wątków, ponieważ nadal nie ma ochrony przed większością warunków wyścigu. Nadal możesz mieć scenariusz, w którym jeden pracownik otrzymuje wartość, daje plony, inny ją modyfikuje, daje, a następnie pierwszy pracownik również ją modyfikuje.


Jeśli potrzebujesz przechowywać jakieś dane globalne podczas żądania, możesz użyć gobiektu Flask . Innym typowym przypadkiem jest obiekt najwyższego poziomu, który zarządza połączeniami z bazą danych. Różnica dla tego typu „globalnego” polega na tym, że jest ona unikalna dla każdego żądania, nie jest używana między żądaniami i istnieje coś, co zarządza konfiguracją i porzuceniem zasobu.

dawidyzm
źródło
30

To nie jest tak naprawdę odpowiedź na bezpieczeństwo wątków globali.

Ale myślę, że warto tu wspomnieć o sesjach. Szukasz sposobu na przechowywanie danych specyficznych dla klienta. Każde połączenie powinno mieć dostęp do własnej puli danych w sposób bezpieczny dla wątków.

Jest to możliwe dzięki sesjom po stronie serwera i są one dostępne w bardzo zgrabnej wtyczce flask: https://pythonhosted.org/Flask-Session/

Jeśli skonfigurujesz sesje, sessionzmienna jest dostępna we wszystkich twoich trasach i zachowuje się jak słownik. Dane przechowywane w tym słowniku są indywidualne dla każdego łączącego się klienta.

Oto krótkie demo:

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

Po pip install Flask-Sessiontym powinieneś być w stanie to uruchomić. Spróbuj uzyskać do niego dostęp z różnych przeglądarek, a zobaczysz, że licznik nie jest między nimi współdzielony.

lhk
źródło
3

Całkowicie akceptując poprzednie opinie za pozytywnymi odpowiedziami i zniechęcając do używania zmiennych globalnych do produkcji i skalowalnej pamięci masowej Flask, w celu tworzenia prototypów lub naprawdę prostych serwerów, działających pod `` serwerem programistycznym '' flask ...

...

Wbudowane typy danych w Pythonie, a ja osobiście użyłem i przetestowałem globalne dict, zgodnie z dokumentacją Pythona są bezpieczne wątkowo . Nie jest bezpieczny dla procesu .

Wstawienia, wyszukiwania i odczyty z takiego (globalnego serwera) dyktu będą prawidłowe z każdej (prawdopodobnie współbieżnej) sesji Flask uruchomionej na serwerze deweloperskim.

Kiedy taka globalna dykta jest kluczowana unikalnym kluczem sesji Flask, może być raczej przydatna do przechowywania danych sesji po stronie serwera, które w innym przypadku nie mieszczą się w pliku cookie (maksymalny rozmiar 4 kB).

Oczywiście taki globalny nakaz serwera powinien być uważnie strzeżony, aby nie urósł zbytnio i nie znalazł się w pamięci. Podczas przetwarzania żądania można zakodować pewien rodzaj utraty ważności „starych” par klucz / wartość.

Ponownie, nie jest to zalecane w przypadku wdrożeń produkcyjnych lub skalowalnych, ale prawdopodobnie jest OK w przypadku lokalnych serwerów zadaniowych, w których oddzielna baza danych jest zbyt duża dla danego zadania.

...

R. Simac
źródło