Python线程中的死锁

2 投票
2 回答
2295 浏览
提问于 2025-04-17 07:05

我正在用Python实现一个简单的端口扫描器。它的工作原理是创建一些工作线程,这些线程会扫描放在队列里的端口。扫描的结果会保存在另一个队列中。当所有端口都扫描完毕后,线程和应用程序应该结束。但是问题来了:对于少量端口,一切都运行得很好,但如果我尝试扫描200个或更多的端口,应用程序就会陷入死锁。我不知道为什么会这样。

class ConnectScan(threading.Thread):
    def __init__(self, to_scan, scanned):
        threading.Thread.__init__(self)
        self.to_scan = to_scan
        self.scanned = scanned

    def run(self):
        while True:
            try:
                host, port = self.to_scan.get()
            except Queue.Empty:
                break
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            try:
                s.connect((host, port))
                s.close()
                self.scanned.put((host, port, 'open'))
            except socket.error:
                self.scanned.put((host, port, 'closed'))
            self.to_scan.task_done()


class ConnectScanner(object):
    def scan(self, host, port_from, port_to):
        to_scan = Queue.Queue()
        scanned = Queue.Queue()
        for port in range(port_from, port_to + 1):
            to_scan.put((host, port))
        for i in range(20):
            ConnectScan(to_scan, scanned).start()
        to_scan.join()

有没有人能看出问题出在哪里?另外,我也希望能得到一些关于如何调试Python中线程问题的建议。

2 个回答

3

我看不出你的代码有什么明显的问题,但目前的情况是,self.to_scan.get() 会一直等待,不会抛出 Queue.Empty 错误。因为你在启动线程之前已经把要扫描的端口放进了队列,所以你可以把它改成 self.to_scan.get(False),这样当所有端口都被处理完后,工作线程就能正确退出。

再加上你有非守护线程(这些线程会在主线程结束后继续保持进程运行),这可能是导致程序卡住的原因。你可以在 to_scan.join() 之后打印一些东西,看看程序是停在这里,还是在进程退出时。

正如 Ray 所说,如果在 self.to_scan.get()self.to_scan.task_done() 之间抛出了除了 socket.error 以外的异常,那么 join 调用会卡住。你可以把这段代码改成使用 try/finally 来确保:

def run(self):
    while True:
        try:
            host, port = self.to_scan.get(False)
        except Queue.Empty:
            break

        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            try:
                s.connect((host, port))
                s.close()
                self.scanned.put((host, port, 'open'))
            except socket.error:
                self.scanned.put((host, port, 'closed'))
        finally:
            self.to_scan.task_done()

一般来说,调试多线程程序是比较棘手的。我尽量避免让程序无限期地阻塞——因为如果超时时间太短导致程序崩溃,总比让它永远等待一个永远不会出现的项目要好。所以我建议你给 self.to_scan.getsocket.connectto_scan.join 的调用设置超时。

使用 logging 来了解事件发生的顺序——打印输出可能会因为不同线程的交错而混乱,但日志记录是线程安全的。

另外,像 这个方法 可以帮助你获取每个线程的当前堆栈跟踪。

我没有使用过支持调试 Python 中多个线程的调试工具,但这里有一些列出的 调试器

1

很可能在 to_scan 队列中的所有项目并没有被全部处理完,而且你没有足够多次调用 task_done 方法,这样就导致 ConnectScanner 没法继续运行。

有没有可能在 ConnectScan.run 运行的时候出现了异常,但你没有捕捉到这个异常,导致你的线程提前结束了?

撰写回答