我有一个程序可以维持与具有周期性心跳的服务器的连接。每隔一段时间,服务器停止响应心跳,我必须重新连接。我用计时器实现了这一点,如果在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()
答案 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__()
以使其正常工作。