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

2 投票
2 回答
710 浏览
提问于 2025-04-16 06:51

我有一个关于Python子解释器初始化(来自Python/C API)和Python id() 函数内部工作原理的问题。更具体地说,是关于在WSGI Python容器(比如与nginx一起使用的uWSGI和在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中测试了这段代码,使用一个主进程和两个工作进程;在mod_wsgi中使用了守护进程模式,两个进程每个进程一个线程。典型的输出是:

pid = 15278
id(A) = 139748093678128
id(a) = 139748093962360
str(a) = one

第一次加载时,然后:

pid = 15282
id(A) = 139748093678128
id(a) = 139748093962360
str(a) = one

第二次加载时,然后:

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

在每次加载中。如你所见,id()(内存地址)在两个进程中都保持不变(上面第一次/第二次加载),而类实例却在不同的上下文中(否则第二次请求会显示“two”而不是“one”)!

我怀疑答案可能在Python文档中有所提示:

id(object):

返回一个对象的“身份”。这是一个整数(或长整数),在对象的生命周期内保证是唯一且不变的。两个生命周期不重叠的对象可能会有相同的id()值。

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

虽然我理解这可能只是Python/C API的一个“聪明”特性,用来解决(或者说是修复第三方扩展模块中缓存对象引用(指针)的问题,但我仍然觉得这种行为与...嗯,常识不太一致。有人能解释一下吗?

我还注意到mod_wsgi在每个进程中都导入模块(也就是两次),而uWSGI只在一次中为两个进程导入模块。由于uWSGI的主进程负责导入,我想它是给子进程提供了该上下文的副本。之后两个工作进程独立工作(深拷贝?),同时似乎使用相同的对象地址。(此外,工作进程在重新加载时会重新初始化为原始上下文。)

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

2 个回答

2

这个内容可以通过一个示例来简单解释。你看,当uwsgi创建一个新进程时,它会复制解释器。这个复制过程有一些有趣的内存特性:

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的问题,而是操作系统的问题。

补充说明:我怀疑mod_wsgi会被导入两次的原因是它通过调用Python来创建更多的进程,而不是通过复制。uwsgi的做法更好,因为它可以使用更少的内存。复制的页面共享是COW(写时复制)。

2

你问的问题有点不太清楚;如果问题更具体一点,我可以给你更简洁的回答。

首先,在CPython中,一个对象的ID其实就是它在内存中的地址。这是很正常的:在同一个进程中,两个对象不能共享同一个地址,而且在CPython中,一个对象的地址是不会改变的,所以用地址作为ID是很合适的。我不明白这有什么不合常理的地方。

接下来,注意到后端进程可以通过两种非常不同的方式来生成:

  • 一种普通的WSGI后端处理器会创建多个进程,然后每个进程会启动一个后端。这种方式简单且不依赖于特定语言,但会浪费很多内存,并且每次都要重复加载后端代码,浪费时间。
  • 一种更高级的后端会先加载一次Python代码,然后在加载完成后再复制服务器进程。这样代码只加载一次,速度更快,内存浪费也大大减少。这就是生产级WSGI服务器的工作方式。

不过,这两种情况下的最终结果是一样的:都是独立的、分叉的进程。

那么,为什么你会得到相同的ID呢?这取决于你使用的是哪种方法。

  • 如果是普通的WSGI处理器,那是因为每个进程基本上在做同样的事情。只要进程在做相同的事情,它们的ID就会趋向于相同;不过到某个时候它们会开始不同,这种情况就不会再发生了。
  • 如果是预加载的后端,那是因为这段初始代码只执行一次,在服务器分叉之前,所以它的ID是保证相同的。

但是,无论是哪种方式,一旦分叉发生,它们就是独立的对象,处于不同的上下文中。不同进程中的对象拥有相同ID并没有什么特别的意义。

撰写回答