【问题标题】:Are global variables thread-safe in Flask? How do I share data between requests?Flask 中的全局变量是线程安全的吗?如何在请求之间共享数据?
【发布时间】:2015-12-25 06:00:08
【问题描述】:

在我的应用程序中,一个普通对象的状态是通过发出请求来改变的,响应取决于状态。

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。跳过了一个数字。

当我扩展我的应用程序时,这真的会发生吗?我应该看看全局变量的哪些替代方案?

【问题讨论】:

    标签: python flask thread-safety


    【解决方案1】:

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

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


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


    一些 WSGI 服务器可能支持 gevent 或其他异步工作者。全局变量仍然不是线程安全的,因为仍然没有针对大多数竞争条件的保护。你仍然可以有这样的场景,一个工人得到一个值,产出,另一个修改它,产出,然后第一个工人也修改它。


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

    【讨论】:

      【解决方案2】:

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

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

      这可以通过服务器端会话实现,并且可以在一个非常简洁的烧瓶插件中使用: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 之后,你应该可以运行它了。尝试从不同的浏览器访问它,您会发现它们之间没有共享计数器。

      【讨论】:

        【解决方案3】:

        请求外部数据源的另一个示例是缓存,例如 Flask-Caching 或其他扩展提供的缓存。

        1. 创建一个文件common.py 并在其中放置以下内容:
        from flask_caching import Cache
        
        # Instantiate the cache
        cache = Cache()
        
        1. 在创建 flask app 的文件中,使用以下代码注册缓存:
        # Import cache
        from common import cache
        
        # ...
        app = Flask(__name__)
        
        cache.init_app(app=app, config={"CACHE_TYPE": "filesystem",'CACHE_DIR': Path('/tmp')})
        
        1. 现在通过导入缓存并执行如下方式在整个应用程序中使用:
        # Import cache
        from common import cache
        
        # store a value
        cache.set("my_value", 1_000_000)
        
        # Get a value
        my_value = cache.get("my_value")
        

        【讨论】:

        • 几个问题。 1. 每个进程都有一个缓存吗? 2. 如果一个缓存不是每个进程唯一的,它是进程安全的吗? (我是否可以获取一个锁,以使其他进程在缓存被另一个进程写入时无法写入缓存?)
        • 你可以在这里阅读更多关于缓存配置的信息@ProQ flask-caching.readthedocs.io/en/latest/index.html#simplecache
        【解决方案4】:

        虽然完全接受之前的赞成答案,并且不鼓励使用全局变量进行生产和可扩展的 Flask 存储,但出于原型设计或真正简单的服务器的目的,在 Flask“开发服务器”下运行...

        ...

        Python内置的数据类型,我个人使用并测试了全局dict、as per Python documentation是线程安全的。不处理安全。

        对于在开发服务器下运行的每个(可能并发的)Flask 会话,从这样的(服务器全局)dict 插入、查找和读取都可以。

        当这样的全局 dict 使用唯一的 Flask 会话密钥作为键时,它对于服务器端存储会话特定数据非常有用,否则不适合 cookie(最大大小 4 kB)。

        当然,应该小心保护这样一个服务器全局字典,以免它变得太大,在内存中。可以在请求处理期间对某种过期的“旧”键/值对进行编码。

        同样,不建议将其用于生产或可扩展部署,但对于本地面向任务的服务器来说可能没问题,因为单独的数据库对于给定任务来说太多了。

        ...

        【讨论】:

          猜你喜欢
          相关资源
          最近更新 更多