You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

触发SIGINT执行shutdown handler时socket recv阻塞致线程无法正常退出

核心问题根因
  • 你在shutdown_handler里关闭的是监听socket self.sock,但接收线程实际是阻塞在已建立连接的self.conn的recv()调用上,关闭监听socket完全不会打断self.conn上的阻塞接收
  • 接收线程的recv()设置了无限阻塞(代码里手动把对应socket超时设为None),没有超时机制的情况下,只要没有对端数据/对端断开信号,线程就会一直卡在recv调用里,导致后续的t.join()永远阻塞,给人一种代码没执行到join步骤的错觉
  • 原代码connect方法中连续写了两个except Exception,第二个超时捕获分支永远不会被触发,属于无效逻辑
修正方案

替换原有Example类实现,保证shutdown流程可正常终止接收线程、释放所有socket资源:

import socket
import threading

class Example(object):
    def __init__(self):
        # 增加线程运行标志位,用于优雅终止接收线程
        self._recv_thread_running = True
        self.conn = None
        self.sock = None
        self.t = None

    def connect(self, host='', port=2000):
        try:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            # 设置地址复用,避免程序重启时端口被占用报错
            self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self.sock.settimeout(10)
            self.sock.bind((host, port))
            self.sock.listen(1)
            # accept受10秒超时控制,超时会抛独立的socket.timeout异常
            self.conn, self.addr = self.sock.accept()
            # 给已建立的连接设置短超时,避免recv永久阻塞
            self.conn.settimeout(1)
            # 重置运行标志
            self._recv_thread_running = True

            self.t = threading.Thread(target=self.recv_msg)
            # 设为守护线程,避免主程序退出时线程残留
            self.t.daemon = True
            self.t.start()
            return True
        
        # 按异常粒度分别捕获,替换原来重复的Exception捕获逻辑
        except socket.timeout:
            print("连接建立超时")
            self._cleanup_resource()
            return False
        except Exception as e:
            print(f"连接建立失败: {e}")
            self._cleanup_resource()
            return False

    def recv_msg(self):
        while self._recv_thread_running:
            try:
                # 因为设置了1秒超时,这里不会永久阻塞,每1秒会检查一次运行标志
                recv_msg = self.conn.recv(1024)
                if not recv_msg:
                    # 对端断开连接时recv会返回空字节,直接退出循环
                    break
                print(recv_msg)
            except socket.timeout:
                # 超时是预期内的,跳过不处理,继续循环检查运行标志
                continue
        # 线程退出前清理连接资源
        if self.conn:
            self.conn.close()

    def shutdown_handler(self):
        # 先修改运行标志,通知接收线程退出
        self._recv_thread_running = False
        try:
            # 主动关闭已建立的连接socket,打断可能正在阻塞的recv
            if self.conn:
                self.conn.shutdown(socket.SHUT_RDWR)
            # 等待接收线程退出,设置超时避免永久卡住
            if self.t:
                self.t.join(timeout=2)
            # 最后关闭监听socket
            if self.sock:
                self.sock.close()
        except Exception as e:
            print(f"关闭流程异常: {e}")
        finally:
            self._cleanup_resource()

    def _cleanup_resource(self):
        # 统一做资源兜底清理,避免资源泄漏
        self._recv_thread_running = False
        for resource in [self.conn, self.sock]:
            if resource:
                try:
                    resource.close()
                except Exception:
                    pass

主程序部分的原有逻辑不需要改动,直接配合修正后的类即可正常响应Ctrl+C信号触发的shutdown流程。

关键改动说明
  • 用线程运行标志位替代原来的while True无限循环,通过标志位控制线程退出流程,避免强杀线程带来的资源泄漏
  • 给conn连接设置1秒的接收超时,让原本永久阻塞的recv()变成周期性唤醒,既能及时接收网络数据,又能定期检查退出标志
  • shutdown流程中优先关闭已建立的连接self.conn,而不是只关监听socket,从根源上打断阻塞的recv调用
  • 所有socket资源在关闭前都做存在性判断,避免空对象调用close报错
  • 给线程join设置2秒的超时兜底,哪怕线程因为异常没来得及退出,主流程也不会永久卡住

内容的提问来源于stack exchange,提问作者rluq

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.23 00:57:06