РЕДАКТИРОВАТЬ : я расширяю ответ, чтобы включить более совершенный пример. В этом посте я обнаружил много враждебности и дезинформации относительно threading v.s. асинхронный ввод-вывод. Поэтому я также добавляю дополнительные аргументы, чтобы опровергнуть некоторую недействительную претензию. Я надеюсь, что это поможет людям выбрать правильный инструмент для правильной работы.
Это дублированный вопрос 3 дня назад.
Python urllib2.open работает медленно, нужен лучший способ чтения нескольких URL-адресов - qaru Python urllib2.urlopen () работает медленно, нужен лучший способ чтения нескольких URL-адресов
Я полирую код, чтобы показать, как получать несколько веб-страниц параллельно с использованием потоков.
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 ()
- лучший способ синхронизировать код. Я думаю, что этот код улучшил все примеры потоковой передачи.
keep-alive соединение
Предложение WoLpH об использовании keep-alive соединения может быть очень полезным, если все ваши URL-адреса указывают на один и тот же сервер.
twisted
Аарон Галлахер - поклонник twisted
фреймворка и враждебно настроен ко всем, кто предлагает нить. К сожалению, многие его утверждения являются дезинформацией. Например, он сказал «-1 для предложения потоков. Это связано с вводом-выводом; потоки здесь бесполезны.«Это противоречит доказательствам, поскольку и Ник Т., и я продемонстрировали прирост скорости от используемого потока. На самом деле приложение, привязанное к вводу-выводу, больше всего выигрывает от использования потока Python (по сравнению с отсутствием прироста в приложении, привязанном к ЦП). Ошибочная критика Аарона в отношении thread показывает, что он довольно смущен параллельным программированием в целом.
Правильный инструмент для правильной работы
Я хорошо осведомлен о проблемах, связанных с параллельным программированием с использованием потоков, python, асинхронного ввода-вывода и т. д. У этого инструмента есть свои плюсы и минусы. Для каждой ситуации есть подходящий инструмент. Я не против скрученных (хотя сам не использовал их). Но я не верю, что мы можем категорически сказать, что резьба - ПЛОХО, а скрученная - ХОРОШО во всех ситуациях.
Например, если требование OP состоит в том, чтобы получить 10 000 веб-сайтов параллельно, предпочтительным будет асинхронный ввод-вывод. Распределение потоков не будет применимо (если, возможно, с Python без стека).
Противодействие Аарона к темам - в основном обобщения. Он не понимает, что это тривиальная задача распараллеливания. Каждая задача независима и не разделяет ресурсы. Так что большая часть его атаки не применима.
Учитывая, что мой код не имеет внешней зависимости, я назову его подходящим инструментом для правильной работы.
Производительность
Я думаю, что большинство людей согласятся с тем, что производительность этой задачи во многом зависит от сетевого кода и внешнего сервера, где производительность кода платформы должна иметь незначительное влияние. Однако тест Aaron показывает прирост скорости на 50% по сравнению с многопоточным кодом. Я думаю, что необходимо реагировать на этот очевидный прирост скорости.
В коде Ника есть очевидная ошибка, которая привела к неэффективности. Но как вы объясните увеличение скорости на 233 мс по сравнению с моим кодом? Я думаю, что даже фанаты скрученного звука воздержатся от поспешных выводов, чтобы отнести это к эффективности скрученного. В конце концов, существует огромное количество переменных вне системного кода, таких как производительность удаленного сервера, сеть, кеширование и реализация различий между urllib2 и скрученным веб-клиентом и так далее.
Чтобы убедиться, что многопоточность 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
Мое тестирование не подтверждает вывод Аарона о том, что многопоточность постоянно медленнее, чем асинхронный ввод-вывод, но с измеримой величиной. Учитывая количество задействованных переменных, я должен сказать, что это недопустимый тест для измерения систематической разницы в производительности между асинхронным вводом-выводом и потоковой передачей.
Используйте витой ! Это делает такие вещи до абсурда проще, чем, скажем, с использованием потоков.
from twisted.internet import defer, reactor
from twisted.web.client import getPage
import time
def processPage(page, url):
# do somewthing here.
return url, len(page)
def printResults(result):
for success, value in result:
if success:
print 'Success:', value
else:
print 'Failure:', value.getErrorMessage()
def printDelta(_, start):
delta = time.time() - start
print 'ran in %0.3fs' % (delta,)
return delta
urls = [
'http://www.google.com/',
'http://www.lycos.com/',
'http://www.bing.com/',
'http://www.altavista.com/',
'http://achewood.com/',
]
def fetchURLs():
callbacks = []
for url in urls:
d = getPage(url)
d.addCallback(processPage, url)
callbacks.append(d)
callbacks = defer.DeferredList(callbacks)
callbacks.addCallback(printResults)
return callbacks
@defer.inlineCallbacks
def main():
times = []
for x in xrange(5):
d = fetchURLs()
d.addCallback(printDelta, time.time())
times.append((yield d))
print 'avg time: %0.3fs' % (sum(times) / len(times),)
reactor.callWhenRunning(main)
reactor.run()
Этот код также работает лучше, чем любое из других опубликованных решений (отредактировано после того, как я закрыл некоторые вещи, которые использовали большую пропускную способность):
Success: ('http://www.google.com/', 8135)
Success: ('http://www.lycos.com/', 29996)
Success: ('http://www.bing.com/', 28611)
Success: ('http://www.altavista.com/', 8378)
Success: ('http://achewood.com/', 15043)
ran in 0.518s
Success: ('http://www.google.com/', 8135)
Success: ('http://www.lycos.com/', 30349)
Success: ('http://www.bing.com/', 28611)
Success: ('http://www.altavista.com/', 8378)
Success: ('http://achewood.com/', 15043)
ran in 0.461s
Success: ('http://www.google.com/', 8135)
Success: ('http://www.lycos.com/', 30033)
Success: ('http://www.bing.com/', 28611)
Success: ('http://www.altavista.com/', 8378)
Success: ('http://achewood.com/', 15043)
ran in 0.435s
Success: ('http://www.google.com/', 8117)
Success: ('http://www.lycos.com/', 30349)
Success: ('http://www.bing.com/', 28611)
Success: ('http://www.altavista.com/', 8378)
Success: ('http://achewood.com/', 15043)
ran in 0.449s
Success: ('http://www.google.com/', 8135)
Success: ('http://www.lycos.com/', 30349)
Success: ('http://www.bing.com/', 28611)
Success: ('http://www.altavista.com/', 8378)
Success: ('http://achewood.com/', 15043)
ran in 0.547s
avg time: 0.482s
И с использованием кода Ника Т, настроенного так, чтобы также дать среднее значение пять и лучше показать результат:
Starting threaded reads:
...took 1.921520 seconds ([8117, 30070, 15043, 8386, 28611])
Starting threaded reads:
...took 1.779461 seconds ([8135, 15043, 8386, 30349, 28611])
Starting threaded reads:
...took 1.756968 seconds ([8135, 8386, 15043, 30349, 28611])
Starting threaded reads:
...took 1.762956 seconds ([8386, 8135, 15043, 29996, 28611])
Starting threaded reads:
...took 1.654377 seconds ([8117, 30349, 15043, 8386, 28611])
avg time: 1.775s
Starting sequential reads:
...took 1.389803 seconds ([8135, 30147, 28611, 8386, 15043])
Starting sequential reads:
...took 1.457451 seconds ([8135, 30051, 28611, 8386, 15043])
Starting sequential reads:
...took 1.432214 seconds ([8135, 29996, 28611, 8386, 15043])
Starting sequential reads:
...took 1.447866 seconds ([8117, 30028, 28611, 8386, 15043])
Starting sequential reads:
...took 1.468946 seconds ([8153, 30051, 28611, 8386, 15043])
avg time: 1.439s
И с использованием кода Вай Ип Тунга:
Fetched 8117 from http://www.google.com/
Fetched 28611 from http://www.bing.com/
Fetched 8386 from http://www.altavista.com/
Fetched 30051 from http://www.lycos.com/
Fetched 15043 from http://achewood.com/
done in 0.704s
Fetched 8117 from http://www.google.com/
Fetched 28611 from http://www.bing.com/
Fetched 8386 from http://www.altavista.com/
Fetched 30114 from http://www.lycos.com/
Fetched 15043 from http://achewood.com/
done in 0.845s
Fetched 8153 from http://www.google.com/
Fetched 28611 from http://www.bing.com/
Fetched 8386 from http://www.altavista.com/
Fetched 30070 from http://www.lycos.com/
Fetched 15043 from http://achewood.com/
done in 0.689s
Fetched 8117 from http://www.google.com/
Fetched 28611 from http://www.bing.com/
Fetched 8386 from http://www.altavista.com/
Fetched 30114 from http://www.lycos.com/
Fetched 15043 from http://achewood.com/
done in 0.647s
Fetched 8135 from http://www.google.com/
Fetched 28611 from http://www.bing.com/
Fetched 8386 from http://www.altavista.com/
Fetched 30349 from http://www.lycos.com/
Fetched 15043 from http://achewood.com/
done in 0.693s
avg time: 0.715s
Должен сказать, мне нравится, что последовательные выборки выполнялись лучше для меня.
Вот пример использования python Threads
. Другие примеры потоков здесь запускают поток для каждого URL-адреса, что является не очень дружелюбным поведением, если оно вызывает слишком много обращений для обработки сервером (например, для пауков часто бывает много URL-адресов на одном и том же хосте)
from threading import Thread
from urllib2 import urlopen
from time import time, sleep
WORKERS=1
urls = ['http://docs.python.org/library/threading.html',
'http://docs.python.org/library/thread.html',
'http://docs.python.org/library/multiprocessing.html',
'http://docs.python.org/howto/urllib2.html']*10
results = []
class Worker(Thread):
def run(self):
while urls:
url = urls.pop()
results.append((url, urlopen(url).read()))
start = time()
threads = [Worker() for i in range(WORKERS)]
any(t.start() for t in threads)
while len(results)<40:
sleep(0.1)
print time()-start
Примечание: Время, указанное здесь, предназначено для 40 URL-адресов и во многом будет зависеть от скорости вашего интернет-соединения и задержки на сервере. Находясь в Австралии, мой пинг> 300 мс
При WORKERS = 1
для запуска потребовалось 86 секунд
При WORKERS = 4
выполнение
заняло 23 секунды.
с WORKERS = 10
потребовалось 10 секунд для запуска
, поэтому загрузка 10 потоков в 8,6 раза быстрее, чем одиночный поток.
Вот обновленная версия, в которой используется очередь. Есть как минимум пара преимуществ.
1. URL-адреса запрашиваются в том порядке, в котором они появляются в списке
.
2. Можно использовать q.join ()
, чтобы определить, когда все запросы выполнены
3.Результаты сохраняются в том же порядке, что и список URL-адресов
from threading import Thread
from urllib2 import urlopen
from time import time, sleep
from Queue import Queue
WORKERS=10
urls = ['http://docs.python.org/library/threading.html',
'http://docs.python.org/library/thread.html',
'http://docs.python.org/library/multiprocessing.html',
'http://docs.python.org/howto/urllib2.html']*10
results = [None]*len(urls)
def worker():
while True:
i, url = q.get()
# print "requesting ", i, url # if you want to see what's going on
results[i]=urlopen(url).read()
q.task_done()
start = time()
q = Queue()
for i in range(WORKERS):
t=Thread(target=worker)
t.daemon = True
t.start()
for i,url in enumerate(urls):
q.put((i,url))
q.join()
print time()-start
Очевидно, что получение веб-страниц займет некоторое время, поскольку вы не получаете доступа ни к чему локальному. Если у вас есть несколько для доступа, вы можете использовать модуль threading
, чтобы запустить пару одновременно.
Вот очень грубый пример
import threading
import urllib2
import time
urls = ['http://docs.python.org/library/threading.html',
'http://docs.python.org/library/thread.html',
'http://docs.python.org/library/multiprocessing.html',
'http://docs.python.org/howto/urllib2.html']
data1 = []
data2 = []
class PageFetch(threading.Thread):
def __init__(self, url, datadump):
self.url = url
self.datadump = datadump
threading.Thread.__init__(self)
def run(self):
page = urllib2.urlopen(self.url)
self.datadump.append(page.read()) # don't do it like this.
print "Starting threaded reads:"
start = time.clock()
for url in urls:
PageFetch(url, data2).start()
while len(data2) < len(urls): pass # don't do this either.
print "...took %f seconds" % (time.clock() - start)
print "Starting sequential reads:"
start = time.clock()
for url in urls:
page = urllib2.urlopen(url)
data1.append(page.read())
print "...took %f seconds" % (time.clock() - start)
for i,x in enumerate(data1):
print len(data1[i]), len(data2[i])
Это был результат, когда я его запустил:
Starting threaded reads:
...took 2.035579 seconds
Starting sequential reads:
...took 4.307102 seconds
73127 19923
19923 59366
361483 73127
59366 361483
Захват данных из потока путем добавления в список, вероятно, не рекомендуется (лучше использовать очередь), но он показывает, что там это разница.
Фактическое ожидание, вероятно, находится не в urllib2
, а в сервере и / или в вашем сетевом подключении к серверу.
Есть 2 способа ускорить это.
multiprocessing
, чтобы упростить задачу.