As variáveis ​​globais são thread-safe no frasco? Como faço para compartilhar dados entre solicitações?

96

Em meu aplicativo, o estado de um objeto comum é alterado por meio de solicitações, e a resposta depende do estado.

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

Se eu executar isso no meu servidor de desenvolvimento, espero obter 1, 2, 3 e assim por diante. Se as solicitações forem feitas de 100 clientes diferentes simultaneamente, algo pode dar errado? O resultado esperado seria que cada 100 clientes diferentes vissem um número exclusivo de 1 a 100. Ou algo assim acontecerá:

  1. Cliente 1 consultas. self.paramé incrementado em 1.
  2. Antes que a instrução de retorno possa ser executada, o thread muda para o cliente 2. self.paramé incrementado novamente.
  3. O encadeamento volta para o cliente 1 e o cliente recebe o número 2, digamos.
  4. Agora o tópico passa para o cliente 2 e retorna o número 3 para ele.

Como havia apenas dois clientes, os resultados esperados eram 1 e 2, não 2 e 3. Um número foi ignorado.

Isso realmente acontecerá quando eu aumentar meu aplicativo? Quais alternativas para uma variável global devo olhar?

Sayantankhan
fonte

Respostas:

91

Você não pode usar variáveis ​​globais para armazenar esse tipo de dados. Além de não ser seguro para threads, não é seguro para o processo , e os servidores WSGI em produção geram vários processos. Não apenas suas contagens estariam erradas se você estivesse usando threads para lidar com solicitações, mas também variariam dependendo de qual processo processou a solicitação.

Use uma fonte de dados fora do Flask para armazenar dados globais. Um banco de dados, memcached ou redis são áreas de armazenamento separadas apropriadas, dependendo de suas necessidades. Se você precisa carregar e acessar dados Python, considere multiprocessing.Manager. Você também pode usar a sessão para dados simples por usuário.


O servidor de desenvolvimento pode ser executado em um único processo e thread. Você não verá o comportamento descrito, pois cada solicitação será tratada de forma síncrona. Habilite threads ou processos e você verá. app.run(threaded=True)ou app.run(processes=10). (No 1.0, o servidor é encadeado por padrão.)


Alguns servidores WSGI podem suportar gevent ou outro trabalhador assíncrono. Variáveis ​​globais ainda não são thread-safe porque ainda não há proteção contra a maioria das condições de corrida. Você ainda pode ter um cenário onde um trabalhador obtém um valor, produz, outro o modifica, produz, então o primeiro trabalhador também o modifica.


Se você precisa armazenar alguns dados globais durante uma solicitação, você pode usar o gobjeto do Flask . Outro caso comum é algum objeto de nível superior que gerencia conexões de banco de dados. A diferença para esse tipo de "global" é que ele é exclusivo para cada solicitação, não é usado entre as solicitações, e há algo gerenciando a configuração e a desativação do recurso.

davidismo
fonte
25

Esta não é realmente uma resposta à segurança do segmento de globais.

Mas acho importante mencionar as sessões aqui. Você está procurando uma maneira de armazenar dados específicos do cliente. Cada conexão deve ter acesso ao seu próprio pool de dados, de maneira threadsafe.

Isso é possível com sessões do lado do servidor e estão disponíveis em um plugin de flask muito bacana: https://pythonhosted.org/Flask-Session/

Se você configurar sessões, uma sessionvariável estará disponível em todas as suas rotas e se comportará como um dicionário. Os dados armazenados neste dicionário são individuais para cada cliente conectado.

Aqui está uma breve demonstração:

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

Depois pip install Flask-Session, você deve conseguir executá-lo. Tente acessá-lo de navegadores diferentes, você verá que o contador não é compartilhado entre eles.

lhk
fonte
2

Embora aceitando totalmente as respostas acima votadas, e desencorajando o uso de globais para produção e armazenamento de frasco escalável, para fins de prototipagem ou servidores realmente simples, rodando sob o 'servidor de desenvolvimento' do frasco ...

... os tipos de dados integrados do python, e eu pessoalmente usei e testei o global dict, de acordo com os documentos do python ( https://docs.python.org/3/glossary.html#term-global-interpreter-lock ) são segmento seguro. Não é seguro para o processo .

As inserções, pesquisas e leituras de tal dicionário (global do servidor) estarão ok em cada sessão de frasco (possivelmente simultânea) em execução no servidor de desenvolvimento.

Quando tal dict global é codificado com chave de sessão de frasco único, pode ser bastante útil para o armazenamento do lado do servidor de dados específicos da sessão, caso contrário, não cabem no cookie (tamanho máximo de 4k).

Obviamente, esse dict global de servidor deve ser cuidadosamente protegido para não ficar muito grande, estando na memória. Algum tipo de expiração dos pares chave / valor 'antigos' pode ser codificado durante o processamento da solicitação.

Novamente, não é recomendado para produção ou implantações escalonáveis, mas possivelmente ok para servidores locais orientados a tarefas onde o banco de dados separado é muito para a tarefa dada

...

R. Simac
fonte