Python select.select泄漏线程

时间:2017-06-03 22:13:21

标签: python multithreading sockets

我有一个程序可以维持与具有周期性心跳的服务器的连接。每隔一段时间,服务器停止响应心跳,我必须重新连接。我用计时器实现了这一点,如果在n秒后没有响应,则会调用重新连接。每次发生这种情况时,我都会泄漏一个线程,随着时间的推移,我最终会耗尽线程。

现在,为了简单的重复,大规模简化,这说明了延迟后重新连接的方式以及如何总是导致线程增加。 如何杀死旧线程/套接字/选择(可能正在等待recv)?

import socket
import select
import threading

class Connection():

    def tick(self):
        print(threading.active_count()) # this increases every 1s!
        # ... certain conditions not met / it's been too long, then:
        self.reconnect()

    def reconnect(self):
        self.socket.shutdown(socket.SHUT_WR)
        self.socket.close()
        self.timer.cancel()
        self.connect()

    def connect(self):
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.socket.connect((IP, TCP_PORT))
        self.timer = threading.Timer(1, self.tick)
        self.timer.start()
        r,_,_ = select.select([self.socket], [], [])

if __name__ == '__main__':
    Connection().connect()

1 个答案:

答案 0 :(得分:2)

我很确定,泄漏任何线程都不是select()。我们假设select()没有返回,即它永远阻止。

在那种情况下

    从计时器线程调用
  • .tick()
  • .tick()在计时器帖子中调用.reconnect()
  • .reconnect()关闭现有套接字。这会导致活动select()调用失败并显示IOError“错误的文件描述符”(这也是您应该真正修复代码的原因)。
  • .reconnect()尝试取消当前计时器。 这没有任何作用,因为计时器已经被触发(我们当前在计时器功能内!)。
  • .reconnect()拨打.connect()并建立一个新的计时器,我们又来了。

所以问题是:这种操作模式在哪里挂起现有的计时器对象?好吧,所有计时器线程都会被IOError来自select()的{​​{1}}终止。这将存储异常的每线程引用。

我的猜测是,这会阻止CPython中的引用计数清理触发,因此只会在垃圾回收期间清除计时器线程。这是不可靠的,因为无法保证计时器线程能够及时清理。

如果您在import gc; gc.collect()的开头添加.connect(),问题(似乎)就会消失。但是,这是一个非解决方案。

为什么不使用timeout参数select()来获得类似的结果而不必使用计时器线程?

r = []
while not r:
    if self.socket:
        self.socket.shutdown(socket.SHUT_WR)
        self.socket.close()

    self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    self.socket.connect((IP, TCP_PORT))
    # select returns empty lists on timeout
    r, _, _ = select.select([self.socket], [], [], 1)

不要忘记在self.socket = None中设置Connection.__init__()以使其正常工作。