子解释器中全局Python对象的唯一性是否无效?
我有一个关于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 个回答
这个内容可以通过一个示例来简单解释。你看,当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(写时复制)。
你问的问题有点不太清楚;如果问题更具体一点,我可以给你更简洁的回答。
首先,在CPython中,一个对象的ID其实就是它在内存中的地址。这是很正常的:在同一个进程中,两个对象不能共享同一个地址,而且在CPython中,一个对象的地址是不会改变的,所以用地址作为ID是很合适的。我不明白这有什么不合常理的地方。
接下来,注意到后端进程可以通过两种非常不同的方式来生成:
- 一种普通的WSGI后端处理器会创建多个进程,然后每个进程会启动一个后端。这种方式简单且不依赖于特定语言,但会浪费很多内存,并且每次都要重复加载后端代码,浪费时间。
- 一种更高级的后端会先加载一次Python代码,然后在加载完成后再复制服务器进程。这样代码只加载一次,速度更快,内存浪费也大大减少。这就是生产级WSGI服务器的工作方式。
不过,这两种情况下的最终结果是一样的:都是独立的、分叉的进程。
那么,为什么你会得到相同的ID呢?这取决于你使用的是哪种方法。
- 如果是普通的WSGI处理器,那是因为每个进程基本上在做同样的事情。只要进程在做相同的事情,它们的ID就会趋向于相同;不过到某个时候它们会开始不同,这种情况就不会再发生了。
- 如果是预加载的后端,那是因为这段初始代码只执行一次,在服务器分叉之前,所以它的ID是保证相同的。
但是,无论是哪种方式,一旦分叉发生,它们就是独立的对象,处于不同的上下文中。不同进程中的对象拥有相同ID并没有什么特别的意义。