全局变量在烧瓶中是线程安全的吗?如何在请求之间共享数据?


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.param增加1。
  2. 在执行return语句之前,线程将切换到客户端2。self.param再次增加。
  3. 线程切换回客户端1,并向客户端返回数字2,例如。
  4. 现在,该线程移至客户端2,并向其返回数字3。

由于只有两个客户,因此预期结果是1和2,而不是2和3。跳过了一个数字。

当我扩展应用程序时,这是否真的会发生?我应该考虑使用什么替代全局变量?

Answers:


91

您不能使用全局变量来保存此类数据。它不仅不是线程安全的,也不是进程安全的,并且生产中的WSGI服务器产生了多个进程。如果您使用线程来处理请求,不仅计数会错误,而且根据处理该请求的进程的不同,计数也会有所不同。

使用Flask外部的数据源来保存全局数据。数据库,内存缓存或Redis都是适合的单独存储区域,具体取决于您的需求。如果您需要加载和访问Python数据,请考虑multiprocessing.Manager。您还可以将会话用于每个用户的简单数据。


开发服务器可以在单线程和进程中运行。您将看不到您描述的行为,因为每个请求都将被同步处理。启用线程或进程,您将看到它。app.run(threaded=True)app.run(processes=10)。(在1.0中,服务器默认为线程化。)


某些WSGI服务器可能支持gevent或其他异步工作器。全局变量仍然不是线程安全的,因为仍然没有针对大多数竞争条件的保护措施。您仍然可以设想这样一个场景,其中一个工作人员获取了一个值,产生了收益,另一个工作人员对其进行了修改,产生了收益,然后第一个工作人员也对其进行了修改。


如果请求期间需要存储一些全局数据,则可以使用Flask的gobject。另一个常见的情况是管理数据库连接的某些顶级对象。这种“全局”类型的区别在于,它对每个请求都是唯一的,请求之间不使用,并且有一些东西可以管理资源的建立和拆除。


24

这并不是对全局变量线程安全的真正答案。

但是我认为在这里提到会议很重要。您正在寻找一种存储特定于客户的数据的方法。每个连接都应该以线程安全的方式访问其自己的数据池。

服务器端会话可以做到这一点,它们可以在非常整齐的烧瓶插件中找到: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内置数据类型,我dict根据python docs(https://docs.python.org/3/glossary.html#term-global-interpreter-lock)亲自使用并测试了global 线程安全。不处理安全。

从开发服务器下运行的每个(可能是并发的)flask会话中,可以从(服务器全局)字典中进行插入,查找和读取。

当使用唯一的flask会话密钥为此类全局字典输入密钥时,它对于会话特定数据的服务器端存储非常有用,否则该会话不适合cookie(最大大小为4k)。

当然,应该谨慎地防止此类服务器全局指令在内存中增长得太大。可以在请求处理期间对某种过期的“旧”键/值对进行编码。

同样,不建议将其用于生产或可伸缩部署,但对于面向本地任务的服务器来说可能不错,因为对于给定任务而言,单独的db过多

...

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.