Python中的并行性无法正常工作

7
我正在使用Python 2.7在gae上开发一个应用程序,其中一个ajax调用从API请求一些数据,单个请求可能需要约200毫秒,但是当我打开两个浏览器并在非常接近的时间内进行两个请求时,它们所需的时间超过了两倍以上。我尝试将所有内容放在线程中,但没有效果...(只有应用程序在线时才会出现这种情况,而不仅仅是在开发服务器上)
因此,我编写了这个简单的测试以查看这是否是Python普遍存在的问题(在忙等待的情况下),以下是代码和结果:
def work():
    t = datetime.now()
    print threading.currentThread(), t
    i = 0
    while i < 100000000:
        i+=1
    t2 = datetime.now()
    print threading.currentThread(), t2, t2-t

if __name__ == '__main__': 
    print "single threaded:"
    t1 = threading.Thread(target=work)
    t1.start()
    t1.join()

    print "multi threaded:"
    t1 = threading.Thread(target=work)
    t1.start()
    t2 = threading.Thread(target=work)
    t2.start()
    t1.join()
    t2.join()

在Mac OS X上,核心i7(4核,8线程),Python2.7的结果是:
single threaded:
<Thread(Thread-1, started 4315942912)> 2011-12-06 15:38:07.763146
<Thread(Thread-1, started 4315942912)> 2011-12-06 15:38:13.091614 0:00:05.328468

multi threaded:
<Thread(Thread-2, started 4315942912)> 2011-12-06 15:38:13.091952
<Thread(Thread-3, started 4323282944)> 2011-12-06 15:38:13.102250
<Thread(Thread-3, started 4323282944)> 2011-12-06 15:38:29.221050 0:00:16.118800
<Thread(Thread-2, started 4315942912)> 2011-12-06 15:38:29.237512 0:00:16.145560

这真的很令人震惊!如果一个线程需要5秒钟才能完成这个任务...我以为同时启动两个线程将需要同样的时间来完成两项任务,但实际上需要近三倍的时间...这使整个线程的想法变得毫无用处,因为按顺序执行它们会更快!在这里我错过了什么?

3
你有没有读过关于 Python 中的全局解释器锁(GIL)的任何内容?如果你想要并行处理,应该看看 multiprocessing,而不是 threading。除非你使用的库专门设计为释放 GIL,否则执行仅限于单个线程。 - g.d.d.c
2
你的基准测试设计得很差。你实际的使用情况将会受到IO限制,而不是CPU限制。Python的GIL在每种情况下的行为都有很大不同。在你的真实使用情况下,线程应该可以正常工作。 - Zach Kelling
1
@g.d.d.c. 多进程在 GAE 中不可用。 - bpgergo
@bpgergo 这是在Python 2.7运行时中。 - Nick Johnson
1
你看到的另一个问题,Mohamed,是App Engine dev_appserver是单线程的,因此多个请求是按顺序执行的。这在生产环境中并非如此。不要期望dev_appserver上的性能指标代表生产行为。 - Nick Johnson
3个回答

9

David Beazley在PyCon 2010上发表了一篇关于这个问题的演讲。正如其他人已经指出的那样,对于某些任务,使用线程尤其是多核心可以导致比单线程执行相同任务更慢的性能。Beazley发现的问题与多个核心之间存在“GIL争夺”有关:

enter image description here

为避免GIL争用,您可以将任务运行在单独的进程中,而不是单独的线程中,以获得更好的结果。multiprocessing模块提供了一种方便的方法来实现这一点,特别是因为multiprocessing API与线程API非常相似。

import multiprocessing as mp
import datetime as dt
def work():
    t = dt.datetime.now()
    print mp.current_process().name, t
    i = 0
    while i < 100000000:
        i+=1
    t2 = dt.datetime.now()
    print mp.current_process().name, t2, t2-t

if __name__ == '__main__': 
    print "single process:"
    t1 = mp.Process(target=work)
    t1.start()
    t1.join()

    print "multi process:"
    t1 = mp.Process(target=work)
    t1.start()
    t2 = mp.Process(target=work)
    t2.start()
    t1.join()
    t2.join()

产量
single process:
Process-1 2011-12-06 12:34:20.611526
Process-1 2011-12-06 12:34:28.494831 0:00:07.883305
multi process:
Process-3 2011-12-06 12:34:28.497895
Process-2 2011-12-06 12:34:28.503433
Process-2 2011-12-06 12:34:36.458354 0:00:07.954921
Process-3 2011-12-06 12:34:36.546656 0:00:08.048761

PS. 正如评论中 zeekay 指出的那样,GIL 争夺只对 CPU 密集型任务严重影响。对于 IO 密集型任务来说,这不应该是一个问题。


这种行为是特定于Python吗?如果是,为什么? - Paul Draper
1
GIL(全局解释器锁)是Python特有的,因此“GIL battles”也是特定于GIL的。我不确定其他语言是否会发生类似的情况。 - unutbu

4

1
我会看一下时间去哪里了。比如说,假设服务器每200毫秒只能回答一个查询。那么你无能为力,因为服务器只能在200毫秒内提供一个回复。

看他的代码,他正在调用 datetime,而不是与服务器通信。 - Paul Draper

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