如何减少多线程Python代码的内存使用?

7
我写了大约50个类,使用 mechanize 和 threading 连接和处理网站。它们都是并行工作的,但彼此之间没有依赖关系。这意味着每个类对应一个网站和一个线程。这不是特别优雅的解决方案,尤其是对于代码管理来说,因为很多代码在每个类中都重复出现(但不足以将其合并为一个类来传递参数,因为某些网站可能需要在方法中间(如“登录”)对检索到的数据进行额外的处理,而其他网站可能不需要)。正如我所说,这不太优雅——但它有效。毋庸置疑,我欢迎所有建议,以更好地编写此代码,而不使用每个网站一个类的方法。增加每个类的附加功能或整体代码管理是一项艰巨的任务。
然而,我发现每个线程占用大约8MB的内存,因此如果有50个运行的线程,我们将看到大约400MB的内存使用量。如果它在我的系统上运行,我不会有问题,但由于它在只有1GB内存的VPS上运行,这开始成为一个问题。您能告诉我如何减少内存使用量,或者有没有其他方法可以同时处理多个网站?
我使用了这个快速测试 Python 程序来测试是否是我的应用程序中存储的数据使用了内存,还是其他原因。正如您在以下代码中所看到的,它只处理 sleep() 函数,但每个线程都使用了8MB的内存。
from thread import start_new_thread
from time import sleep

def sleeper():
    try:
        while 1:
            sleep(10000)
    except:
        if running: raise

def test():
    global running
    n = 0
    running = True
    try:
        while 1:
            start_new_thread(sleeper, ())
            n += 1
            if not (n % 50):
                print n
    except Exception, e:
        running = False
        print 'Exception raised:', e
    print 'Biggest number of threads:', n

if __name__ == '__main__':
    test()

当我运行此代码时,输出结果如下:
50
100
150
Exception raised: can't start new thread
Biggest number of threads: 188

通过删除 running = False 这行代码,我可以使用 shell 命令 free -m 来测量可用内存:

             total       used       free     shared    buffers     cached
Mem:          1536       1533          2          0          0          0
-/+ buffers/cache:       1533          2
Swap:            0          0          0

实际计算表明,每个线程大约占用8MB的内存。具体方法是将上述测试应用程序运行期间使用的内存与之前使用的内存之差除以最大线程数。

这可能仅仅是分配的内存,因为通过查看top,可以发现python进程仅使用了约0.6%的内存。


你如何准确地测量内存使用情况?我猜想,那8MB并不是真正分配给每个单独的线程的。这8MB中的很大一部分可能在线程之间共享(只是猜测..)? - Frunsi
1
这是一个托管吗?ulimit -u呢?还有ulimit -a呢? - dani herrera
1
@Andrew:所以,你大致测量了Python中单个线程的开销。毕竟,8MB在今天看来是合理的... - Frunsi
当然可以 :) 我会等待别人对我面临的问题所说的话。尽管通过降低堆栈大小限制可以部分地(且暂时地)解决这个特定的问题,但远远不是一个好的解决方案(而且我不是专家,但更改一个Python程序的系统设置可能会导致其他副作用)。如果有其他解决方案,像Frunsi提出的一样,我欢迎它们 :) 正如我之前写的那样,使用“ulimit -s”命令降低堆栈大小限制只在每个会话中是临时的。 - Gargauth
1
@andrew,ulimit -s 可以通过 limit pam 模块 进行修复,每个参数都有软限制和硬限制。此外,您可以在 bash.rc 中为每个用户分配自定义限制。例如,请参阅 Oracle 文档,Oracle 服务器需要自定义这些参数才能正常工作 - dani herrera
显示剩余4条评论
4个回答

6

如果资源管理是一个问题,只需使用线程池并调整池限制。 - Giacomo Lacava
谢谢!看起来Gevent就是我一直在寻找的东西。 - Gargauth

2
“每个请求一个线程”的方法对于许多用例来说是可行且容易的。但是,它会占用大量资源(正如您所体验的那样)。
更好的方法是使用异步方式,但不幸的是,这种方式更加复杂。
以下是一些相关的提示:

谢谢,非常感谢。我之前了解过Twisted,但很遗憾我对它不是很了解,看起来我可能无法与mechanize一起使用它。我会看看是否能让mechanize与asyncore一起工作。 - Gargauth
毕竟,“完美”的解决方案将是线程池和每个CPU核心一个线程的混合(以利用它们来处理任务)以及异步IO。实际的解决方案将取决于您的实际应用程序代码。也许,甚至基于“select”的简单解决方案对您来说就足够了。 - Frunsi
1
这意味着:在你的线程中,发送一堆请求,然后进入一个循环,在适当的套接字上进行“select”,并逐个处理任何传入的数据...等等。毕竟,操作系统无论如何都关心套接字IO,你的任务是以最有效的方式与操作系统进行接口。 - Frunsi
事实上,我所拥有的代码非常简单。每个子类都相当相似,只是URL、名称、值等不同,有时数据处理方式也不同。它们完全不依赖于彼此。我想要的就是并发运行它们,等待它们完成工作,然后退出。我读到的所有解决方案都是针对更复杂的事情,我认为这些方案都过于复杂了。我无法相信没有人开发出一个模块,用于简单的异步/线程执行不互相依赖的类或函数。 - Gargauth
@Andrew:所有必需的代码和框架都已经存在,现在你只需要使用它们就可以了 ;) - Frunsi

1

解决方案是替换以下代码:

1)做某事。
2)等待某事发生。
3)做其他事情。

使用以下代码:

1)做某事。
2)安排当某事发生时,会执行其他操作。
3)完成。

在其他地方,您有几个线程执行以下操作:

1)等待任何事情发生。
2)处理发生的任何事情。
3)返回步骤1。

在第一种情况下,如果您正在等待50件事情发生,那么您将有50个线程坐在那里等待这50件事情发生。在第二种情况下,您只有一个线程等待,该线程将执行需要完成的这50件事情中的任何一件。

因此,不要使用线程等待单个事件的发生。相反,安排当该事件发生时,其他线程将执行接下来需要完成的任务。


0

我并不是Python专家,但也许可以拥有一些线程池来控制活跃线程的总数,并在前一个线程完成后将“请求”移交给线程。请求不必是完整的线程对象,只需提供足够完成请求的数据即可。

您还可以将其结构化,以便使用具有N个线程的线程池A对网站进行ping,一旦检索到数据,就将数据移交给具有Y个线程的线程池B来处理数据。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接