子解释器中全局Python对象的唯一性无效?

时间:2010-11-10 16:27:34

标签: mod-wsgi uwsgi python

我对Python子解释器初始化(来自Python / C API)和Python id()函数的内部工作有疑问。更准确地说,关于WSGI Python容器中的全局模块对象的处理(例如在Apache上使用nWSx和mod_wsgi的uWSGI)。

以下代码在两个提到的环境中都按预期工作(隔离),但我无法向自己解释为什么id()函数总是为每个变量返回相同的值,无论执行它的进程/子解释器如何。

from __future__ import print_function
import os, sys

def log(*msg):
    print(">>>", *msg, file=sys.stderr)

class A:
    def __init__(self, x):
        self.x = x
    def __str__(self):
        return self.x
    def set(self, x):
        self.x = x

a = A("one")
log("class instantiated.")

def application(environ, start_response):

    output = "pid = %d\n" % os.getpid()
    output += "id(A) = %d\n" % id(A)
    output += "id(a) = %d\n" % id(a)
    output += "str(a) = %s\n\n" % a

    a.set("two")

    status = "200 OK"
    response_headers = [
        ('Content-type', 'text/plain'), ('Content-Length', str(len(output)))
    ]
    start_response(status, response_headers)

    return [output]

我已经在uWSGI中使用一个主进程和2个工作程序测试了此代码;在mod_wsgi中使用带有两个进程和每个进程一个线程的deamon模式。典型的输出是:

  

pid = 15278
  id(A)= 139748093678128
  id(a)= 139748093962360
  str(a)=一个

首次加载,然后:

  

pid = 15282
  id(A)= 139748093678128
  id(a)= 139748093962360
  str(a)=一个

秒,然后

  

pid = 15278 | pid = 15282
  id(A)= 139748093678128
  id(a)= 139748093962360
  str(a)=两个

彼此。如您所见,类和类实例的id()(内存位置)在两个进程(上面的第一个/第二个加载)中保持相同,而同时类实例生活在一个单独的环境中(否则第二个请求将显示“两个”而不是“一个”)!

我怀疑Python文档会暗示答案:

  

id(object)

     

返回对象的“标识”。这是一个整数(或长整数)   对于此对象的生命周期,保证唯一且恒定。二   生命周期不重叠的对象可能具有相同的id()值。

但如果确实是这个原因,我会对下一个声称id()值是对象地址的声明感到不安!

虽然我很欣赏这可能只是一个Python / C API“聪明”功能解决(或者更确切地说修复problem of caching object references (pointers) in 3rd party extension modules,我仍然发现这种行为不一致......好吧,常识。有人可以解释一下吗?

我还注意到mod_wsgi在每个进程中导入模块(即两次),而uWSGI仅为两个进程导入模块一次。由于uWSGI主进程执行导入,我想它会为子进程播放该上下文的副本。两个工作人员之后独立工作(深拷贝?),同时使用相同的对象地址。 (此外,工作人员在重新加载时会重新初始化为原始上下文。)

我为这么长的帖子道歉,但我想提供足够的细节。 谢谢!

2 个答案:

答案 0 :(得分:2)

这很容易通过演示来解释。你看,当uwsgi创建一个新进程时,它会分叉解释器。现在,forks有一些有趣的内存属性:

import os, time

if os.fork() == 0:
    print "child first " + str(hex(id(os)))
    time.sleep(2)
    os.attr = 'test'
    print "child second " + str(hex(id(os)))
else:
    time.sleep(1)
    print "parent first " + str(hex(id(os)))
    time.sleep(2)
    print "parent second " + str(hex(id(os)))
    print os.attr

输出:

child first 0xb782414cL
parent first 0xb782414cL
child second 0xb782414cL
parent second 0xb782414cL
Traceback (most recent call last):
  File "test.py", line 13, in <module>
    print os.attr
AttributeError: 'module' object has no attribute 'attr'

虽然对象似乎驻留在同一个内存地址中,但它们是不同的对象,但这不是python,而是os。

编辑:我怀疑mod_wsgi导入两次的原因是它通过调用python而不是forking来创建进一步的进程。 uwsgi的方法更好,因为它可以使用更少的内存。 fork的页面共享是COW(写入时复制)。

答案 1 :(得分:1)

你所要求的并不完全清楚;如果问题更具体,我会给出一个更简洁的答案。

首先,对象的id实际上 - 至少在CPython中 - 它在内存中的地址。这是完全正常的:同一进程中的两个对象不能共享一个地址,并且对象的地址在CPython中永远不会改变,因此该地址作为一个id整齐地工作。我不知道这是如何违反常识的。

接下来,请注意,后端进程可能以两种截然不同的方式生成:

  • 通用WSGI后端处理程序将派生进程,然后每个进程将启动后端。这很简单,与语言无关,但浪费了大量内存,浪费了重复加载后端代码的时间。
  • 更高级的后端将加载Python代码一次,然后在加载后分叉服务器的副本。这导致代码只加载一次,速度更快,并显着减少内存浪费。这就是生产质量的WSGI服务器的工作方式。

但是,这两种情况的最终结果都是相同的:单独的分叉流程。

那么,为什么你最终得到相同的ID?这取决于上述哪种方法正在使用中。

  • 使用通用的WSGI处理程序,它只是因为每个进程基本上都做同样的事情而发生。只要进程做同样的事情,他们往往会得到相同的ID;在某些时候他们会分歧,这将不再发生。
  • 使用预加载后端,它正在发生,因为这个初始代码只发生一次,在服务器分叉之前,所以它保证具有相同的ID。

然而,无论哪种方式,一旦fork发生,它们就是单独的对象,在不同的上下文中。对具有相同ID的单独进程中的对象没有意义。