1

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

以下代码在上述两种环境中都按预期(隔离)工作,但我无法向自己解释为什么该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 中使用具有两个进程和每个进程一个线程的守护程序模式。典型的输出是:

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 的“聪明”功能,可以解决(或更确切地说是修复)在 3rd 方扩展模块中缓存对象引用(指针)的问题,但我仍然发现这种行为不一致与...嗯,常识。有人可以解释一下吗?

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

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

4

2 回答 2

2

您在问什么并不完全清楚。如果问题更具体,我会给出更简洁的答案。

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

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

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

然而,这两种情况的最终结果是相同的:分离的、分叉的进程。

那么,为什么您最终会使用相同的 ID?这取决于使用上述哪种方法。

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

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

于 2010-11-10T17:11:20.477 回答
2

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

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 而不是分叉来创建进一步的进程。uwsgi 的方法更好,因为它可以使用更少的内存。fork 的页面共享是 COW(写时复制)。

于 2010-11-10T17:31:28.957 回答