EDIT:我正在扩展答案以包含一个更精美的示例。我在这篇文章中发现了很多关于线程与线程的敌意和错误信息。异步 I/O。因此我还添加了更多的论据来反驳某些无效的主张。我希望这将帮助人们为正确的工作选择正确的工具。
这是三天前的一个问题的重复。
Python urllib2.open 很慢,需要更好的方法来读取多个网址 - 代码日志Python urllib2.urlopen() 很慢,需要更好的方法来读取多个 url https://stackoverflow.com/questions/3472515/python-urllib2-open-is-slow-need-a-better-way-to-read-several-urls/3472905#3472905
我正在完善代码以展示如何使用线程并行获取多个网页。
import time
import threading
import Queue
# utility - spawn a thread to execute target for each args
def run_parallel_in_threads(target, args_list):
result = Queue.Queue()
# wrapper to collect return value in a Queue
def task_wrapper(*args):
result.put(target(*args))
threads = [threading.Thread(target=task_wrapper, args=args) for args in args_list]
for t in threads:
t.start()
for t in threads:
t.join()
return result
def dummy_task(n):
for i in xrange(n):
time.sleep(0.1)
return n
# below is the application code
urls = [
('http://www.google.com/',),
('http://www.lycos.com/',),
('http://www.bing.com/',),
('http://www.altavista.com/',),
('http://achewood.com/',),
]
def fetch(url):
return urllib2.urlopen(url).read()
run_parallel_in_threads(fetch, urls)
正如您所看到的,应用程序特定的代码只有 3 行,如果您积极的话,可以将其折叠为 1 行。我认为没有人可以证明他们的说法是合理的,即这是复杂且无法维护的。
不幸的是,这里发布的大多数其他线程代码都有一些缺陷。他们中的许多人都会主动轮询以等待代码完成。join()
是同步代码的更好方法。我认为这段代码已经改进了迄今为止的所有线程示例。
保持连接
如果所有 URL 都指向同一服务器,WoLpH 关于使用保持活动连接的建议可能非常有用。
twisted
亚伦·加拉格尔是以下人士的粉丝twisted
框架,他敌视任何建议线程的人。不幸的是,他的许多说法都是错误信息。例如,他说“-1 表示建议线程。这是 IO 绑定的;线程在这里毫无用处。”这与证据相反,因为 Nick T 和我都证明了使用线程的速度增益。事实上,I/O 密集型应用程序可以从使用 Python 线程中获得最大收益(而 CPU 密集型应用程序则没有任何收益)。 Aaron 对线程的误导性批评表明他对一般的并行编程相当困惑。
正确的工具适合正确的工作
我很清楚与使用线程、Python、异步 I/O 等进行并行编程有关的问题。每个工具都有其优点和缺点。对于每种情况都有合适的工具。我并不反对扭曲(尽管我自己还没有部署过)。但我不相信我们可以断然地说在所有情况下线都是坏的,而扭曲的都是好的。
例如,如果OP的要求是并行获取10,000个网站,则异步I/O将是首选。线程是不合适的(除非使用无堆栈Python)。
Aaron 对线程的反对大多是概括性的。他没有意识到这是一个微不足道的并行化任务。每个任务都是独立的,不共享资源。所以他的大部分攻击都不适用。
鉴于我的代码没有外部依赖性,我将其称为适合正确工作的正确工具。
表现
我想大多数人都会同意这个任务的性能在很大程度上取决于网络代码和外部服务器,其中平台代码的性能影响可以忽略不计。然而,Aaron 的基准测试显示,线程代码的速度提高了 50%。我认为有必要对这种明显的速度增益做出反应。
在尼克的代码中,有一个明显的缺陷导致效率低下。但是你如何解释我的代码获得的 233 毫秒的速度增益?我想即使是扭曲的粉丝也不会轻易下结论,将这归因于扭曲的效率。毕竟,系统代码之外还有大量的变量,例如远程服务器的性能、网络、缓存以及 urllib2 和twisted web 客户端之间的差异实现等。
为了确保 Python 的线程不会导致大量低效率,我做了一个快速基准测试来生成 5 个线程,然后生成 500 个线程。我可以很轻松地说,生成 5 个线程的开销可以忽略不计,并且无法解释 233 毫秒的速度差异。
In [274]: %time run_parallel_in_threads(dummy_task, [(0,)]*5)
CPU times: user 0.00 s, sys: 0.00 s, total: 0.00 s
Wall time: 0.00 s
Out[275]: <Queue.Queue instance at 0x038B2878>
In [276]: %time run_parallel_in_threads(dummy_task, [(0,)]*500)
CPU times: user 0.16 s, sys: 0.00 s, total: 0.16 s
Wall time: 0.16 s
In [278]: %time run_parallel_in_threads(dummy_task, [(10,)]*500)
CPU times: user 1.13 s, sys: 0.00 s, total: 1.13 s
Wall time: 1.13 s <<<<<<<< This means 0.13s of overhead
对我的并行获取的进一步测试显示,17 次运行中响应时间存在巨大差异。 (不幸的是,我没有扭曲验证亚伦的代码)。
0.75 s
0.38 s
0.59 s
0.38 s
0.62 s
1.50 s
0.49 s
0.36 s
0.95 s
0.43 s
0.61 s
0.81 s
0.46 s
1.21 s
2.87 s
1.04 s
1.72 s
我的测试并不支持 Aaron 的结论,即线程始终比异步 I/O 慢很多。考虑到涉及的变量数量,我不得不说这不是衡量异步 I/O 和线程之间系统性能差异的有效测试。