在 python 协程中,socket.socket() 可能 return 一个占用的文件描述符

In python co-routine, socket.socket() may return an occupied file descriptor

我有一段python练习python协程的代码。 正如 A. Jesse Jiryu Davis 所解释的那样。

但是我收到了错误信息: KeyError: '368 (FD 368) is already registered' 在行 selector.register(s.fileno(), EVENT_WRITE).

此错误是由于两次调用 socket.socket() 返回相同的文件描述符造成的。实际上,这个文件描述符368在上一次调用时已经分配,​​但在第二次调用时仍然返回。

这次错误信息就没有了! 如果你想自己 运行 代码,你可以在 Task.step 方法中取消注释 arr.append(self.init) 来查看错误-免费输出。

EDIT 如果我显式调用 python 垃圾收集,偶尔这个错误会消失。 但为什么偶尔?

经过几天的搜索和阅读 python 文档,我仍然不知道为什么会这样。我只是错过了一些 'python Gotchas',是吗?

我正在使用 python 3.6 进行测试。代码如下,我删除了所有不相关的代码,使以下代码更加准确和切合主题:

#! /usr/bin/python

from selectors import DefaultSelector, EVENT_WRITE
import socket
import gc

selector = DefaultSelector()
arr = [1, 2, 3]

class Task:

    def __init__(self, gen):
        self.gen = gen
        self.step()

    def step(self):
        next(self.gen)
        # arr.append(self.__init__)

def get(path, count = 0):
    s = socket.socket()
    print(count, 'fileno:', s.fileno())
    s.connect(('www.baidu.com', 80))
    selector.register(s.fileno(), EVENT_WRITE)
    yield

Task(get('/foo',1))
gc.collect()
Task(get('/bar',2))

注:

  1. 你的get('/foo', 1)是一个匿名生成器对象,Task(get('/foo', 1))是一个匿名Task对象。
  2. GC 是 python 垃圾的缩写 collection/collector

您原代码的引用链为:

selector --> # socket_fd
anonymous Task(get('/foo', 1)) --> anonymous get('/foo', 1) --> s

所以匿名Task(get('/foo', 1))对象一完成就被GC回收了。这是因为:

Python GC will collect the memory of an object as soon as it finds the object's reference count == 0. But python GC is not running as a thread so maybe not the moment right after the object's reference count decreases to 0.

那么匿名的get('/foo', 1)就会被收集起来,然后就是s。在这里,s 被收集、关闭并且其对应的套接字#fd 号(在您的示例中为#368)已被释放。

但是套接字#fd号(#368)已经注册到selector

然后你运行Task(get('/bar',2)),一个新的socket s试图申请一个“新”的fd,因为#368可用(只要你系统中的其他进程还没有声明它), 你将得到 #368 as socket fd agian.

在 Task.step()

中取消评论 arr.append(self.__init__)

在 Task.step() 方法中取消注释 arr.append(self.__init__) 后,全局 arr 持有对 Task(get('/foo', 1)) 的引用。然后 Task(get('/foo', 1)) 引用了 get('/foo', 1)。然后 get('/foo', 1) 引用了您的本地套接字 s。这个参考链是这样的:

arr --> anonymous Task(get('/foo', 1)) --> anonymous get('/foo', 1) --> s

arr通过你的程序有效,所以s不会被GC回收。以后s = socket.socket()不会得到相同的fd,因为它仍然被s持有。

使用s代替s.fileno()

如果您使用 selector.register(s ..) 而不是 selector.register(s.fileno()..),则全局 selector 将保存对本地 s 的引用,引用链为:

selector --> s

虽然那两个匿名对象没有了,但是你的get('/foo', 1))::sget('/bar', 2))::s仍然被全局selector持有。所以不用担心这两个 fd 不会碰撞。

循环引用?

答案是否定的,你的情况与循环引用无关

gc.collect?

好吧,换成time.sleep(0.02)你会观察到同样的现象。这可能是由于:

  1. 套接字来来去去由系统的其他进程驱动。
  2. python GC 线程可能需要一些时间才能“发现”s 应该被收集,或者线程正在收集。

@allen He,非常感谢您的回复。

  1. 我进一步研究,结论是:问题不是gc引起的。

The "garbage collector" referred to in gc is only used for resolving circular references. In Python (at least in the main C implementation, CPython) the main method of memory management is reference counting. In my code, the result of Task() has no references, so will always be disposed immediately. There's no way of preventing that no matter you use gc.disable() or anything else.

来自@Daniel Roseman 的配额,请参见此处:

how to prevent Python garbage collection for anonymous objects?

  1. 还有一个,PythonGC不是线程。相反,它是同步的

参见:Why python doesn't have Garbage Collector thread?

  1. 所以我问题标题的最终答案是:

没有,socket.socket() 不会return占用文件描述符。如果它 return 编辑了一个 "occupied" fd,这意味着前一个已经发布。