【问题标题】:Cannot close listening socket to abort accept()/select() from a separate thread无法关闭侦听套接字以从单独的线程中止接受()/选择()
【发布时间】:2020-03-08 15:41:53
【问题描述】:

我正在编写一个 Python (v3.7.3) 套接字服务器,我想为其使用阻塞 I/O。我使用select() 没有超时来接受新客户以及从中读取。我可以关闭监听套接字以中止 select(),并捕获 OSError 作为停止执行的指示。

但是,当在单独的线程中运行时,这似乎不起作用,我不明白为什么。

我知道还有其他方法可以完成此操作,例如使用超时、为 select() 使用虚拟套接字或与侦听器建立虚拟连接以将其唤醒。但是这些都在某种程度上违背了使用 select() 的目的,并且在单线程中运行时没有必要。

这是一个重现问题的基本示例,在我的实际代码中,它仅代表多个线程中的一个(因此,我首先使用线程):

#!/usr/bin/env python3

import signal
import socket
import threading


class SocketCloseTest:
    """Simple test case for using socket.close() to abort select.select()"""

    def __init__(self, port, address=None):
        self.port = port
        self.address = address or ''

        self.socket = None

    def stop(self):
        """Close listening socket to stop select.select()"""

        if self.socket:
            print("Closing listener", self.socket)
            self.socket.close()
            print("Listener closed", self.socket)

    def threaded_run(self):
        """Run test in a separate thread"""

        thread = threading.Thread(target=self.run)
        print("Starting sub-thread")
        thread.start()
        thread.join()
        print("Sub-thread ended")

    def run(self):
        """Run test"""

        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Reuse port for quick re-launch of the application
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind((self.address, self.port))
        print("Starting listener")
        self.socket.listen()

        try:
            print("select() started")
            r, w, e = select.select([self.socket], [], [])
        except OSError:
            print("select() aborted")
        else:
            print("select() completed")


if __name__ == '__main__':
    tester = SocketCloseTest(5000, address='')

    # Set up signal handler for Ctrl-C
    def signal_handler(signum, frame):
        print("Received signal {}".format(signum))
        tester.stop()
    signal.signal(signal.SIGINT, signal_handler)

    # This works
    tester.run()

    # This doesn't work
    # tester.threaded_run()

    print("Main thread ended")

当使用test.run() 时,它会按预期运行,结果如下:

开始监听
选择开始
^C收到信号2
关闭监听器
监听器关闭
选择取消
主线程结束

但是,当使用tester.threaded_run() 运行时,它只会挂起对 select() 的调用应该中止的位置。奇怪的是,此时将作业置于后台会导致代码继续运行:

启动子线程
启动监听器
选择开始
^C收到信号2
关闭监听器
监听器关闭
--在这里按下Ctrl-Z 可以在shell 中暂停作业--

$ bg
--Shell 在后台报告作业--
选择取消
子线程结束
主线程结束

谢谢……

  • 编辑提到accept() 患有完全相同的症状。

【问题讨论】:

    标签: python multithreading sockets select


    【解决方案1】:

    close() 在多线程情况下不会做你想做的事。请改用您描述的其他机制之一。

    在单线程情况下,控制权返回到select(),它会重新启动并在现在关闭的文件描述符上通知 EBADF。 (当然,这是非常危险的,因为 fd #3 可能随时被任何其他线程甚至是复杂的信号处理程序回收,尽管您的玩具程序看起来是安全的。)在多线程情况下,close()只是不会唤醒您的 select()ing 线程。

    Python docs warn:

    注意: close() 释放与连接关联的资源,但不一定立即关闭连接。如果您想及时关闭连接,请在close()之前调用shutdown()

    其实这是一个比较棘手的和平台相关的问题。摘录2008 article in the venerable Dr Dobb's

    在某些操作系统上,[ shutdown() 而不是 close() ] 也是唯一可行的解​​决方案:在 FreeBSD 上,close() 没有 shutdown() 不会唤醒在 read()select()....中等待的进程。 p>

    另一个需要考虑的问题是,由 shutdown()close() 关闭可能不会被视为操作系统中的读取事件....

    但是,shutdown() 仅适用于已建立连接的套接字,不适用于侦听新连接的套接字,也不适用于其他类型的文件描述符....

    (无论如何,在我的系统上,shutdown()确实唤醒了select()ing 线程。)

    【讨论】:

    • 感谢您提供经过充分研究的出色答案。考虑到症状,这是完全有道理的。看起来确实需要对所涉及的系统(操作系统、CPython 和相关代码)有更深入的了解才能将它们拼凑在一起,而你做得很好。
    • 虽然我知道你建议不要这样做,但我想我会看到 shutdown() 如何影响我的场景。像你一样,它有效,但对我来说只是部分时间;其他时候它仍然挂起,大概是因为比赛条件。虽然知道我没有达到 100% 的效率让我很痛苦,但我通过使用超时和循环停止标志来缓解。
    • @PiranhaPhish 谢谢!我认为这是一个很好的问题,并惊讶地发现情况如此令人担忧。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-01
    • 2011-12-10
    • 1970-01-01
    • 2017-04-27
    • 2011-04-20
    • 1970-01-01
    相关资源
    最近更新 更多