Краулер слишком медленный? Давайте попробуем ускориться с помощью асинхронных сопрограмм!

задняя часть Python сервер рептилия

1. Введение

При выполнении некоторых задач с интенсивным вводом-выводом программа часто блокируется из-за ожидания ввода-вывода. Например, в веб-сканере, если мы используем библиотеку запросов для отправки запросов, если скорость ответа веб-сайта слишком низкая, программа ждала ответа веб-сайта, и, наконец, эффективность ее сканирования очень и очень низкая.

Чтобы решить проблему такого рода, в этой статье будет обсуждаться способ ускорения асинхронных сопрограмм в Python, который очень эффективен для задач с интенсивным вводом-выводом. Если его применить к поисковым роботам, эффективность сканирования может быть повышена даже в сотни раз.

Примечание. Сопрограмма в этой статье реализована с использованием async/await, для которого требуется Python 3.5 и выше.

2. Базовое понимание

Прежде чем понять асинхронные сопрограммы, мы должны сначала понять некоторые основные понятия, такие как блокировка и неблокировка, синхронность и асинхронность, многопроцессность и сопрограммы.

2.1 Блокировка

Состояние блокировки относится к состоянию, в котором программа приостанавливается, когда требуемые вычислительные ресурсы не получены. Говорят, что программа заблокирована при выполнении операции, когда она не может продолжать выполнять другие действия, ожидая завершения операции.

Распространенными формами блокировки являются: блокировка сетевого ввода-вывода, блокировка дискового ввода-вывода, блокировка пользовательского ввода и т. д. Блокировка вездесуща, в том числе когда ЦП переключает контексты, все процессы толком ничего сделать не могут, и они тоже будут заблокированы. В случае многоядерного ЦП нельзя использовать ядро, выполняющее операцию переключения контекста.

2.2 Неблокирующий

Программа не блокируется в процессе ожидания операции и может продолжать выполняться для выполнения других действий, тогда программа считается неблокирующей в этой операции.

Неблокировки не существует ни на каком программном уровне и ни при каких обстоятельствах.
Неблокирующее состояние программы возможно только в том случае, если ее уровень инкапсуляции может охватывать независимые модули подпрограммы.

Существование неблокировки обусловлено существованием блокировки Именно из-за трудоемкости и неэффективности, вызванных блокировкой операции, мы делаем ее неблокирующей.

2.3 Синхронизация

Чтобы выполнить определенную задачу, различные программные блоки должны полагаться на определенный метод связи для координации в процессе выполнения, и говорят, что эти программные блоки выполняются синхронно.

Например, для обновления запасов товаров в торговой системе необходимо использовать «блокировку строк» ​​в качестве коммуникационного сигнала, чтобы разные запросы на обновление принудительно ставились в очередь и выполнялись последовательно, а операция обновления запасов была синхронной.

Короче говоря, синхронизация означает порядок.

2.4 Асинхронный

Чтобы выполнить определенную задачу, способ, которым задача может быть выполнена без связи и координации между различными программными модулями в процессе, несвязанные программные модули могут быть асинхронными.

Например, сканер загружает веб-страницу. Как только планировщик вызывает загрузчик, другие задачи могут быть запланированы без необходимости поддерживать связь с загрузчиком для координации поведения. Такие операции, как загрузка и сохранение различных веб-страниц, не связаны друг с другом, и нет необходимости во взаимном уведомлении и координации. Момент завершения этих асинхронных операций неизвестен.

Короче говоря, асинхронность означает неисправность.

2.5 Многопроцессорность

Многопроцессорность — это использование многоядерного преимущества процессора для одновременного выполнения нескольких задач, что может значительно повысить эффективность выполнения.

2.6 Сопрограммы

Coroutine, английское название Coroutine, также известное как микропоток, волокно, сопрограмма — это облегченный поток в пользовательском режиме.

Сопрограмма имеет свой собственный регистровый контекст и стек. Когда сопрограмма запланирована на переключение, сохраните контекст регистра и стек в других местах, а при обратном переключении восстановите ранее сохраненный контекст регистра и стек. Поэтому сопрограмма может сохранять состояние последнего вызова, то есть определенную комбинацию всех локальных состояний, и каждый раз, когда процесс реентерабельен, это эквивалентно входу в состояние последнего вызова.

Сопрограмма — это, по сути, один процесс. По сравнению с несколькими процессами, сопрограмма не требует накладных расходов на переключение контекста потока или накладные расходы на блокировку и синхронизацию атомарных операций. Модель программирования также очень проста.

Мы можем использовать сопрограммы для реализации асинхронных операций.Например, в сценарии веб-краулера после отправки запроса нам нужно подождать определенное время, чтобы получить ответ, но на самом деле во время этого процесса ожидания программа может делать много других вещей и ждать ответа. Получив его, переключиться обратно, чтобы продолжить обработку, чтобы можно было полностью использовать ЦП и другие ресурсы, что является преимуществом асинхронных сопрограмм.

3. Асинхронное использование сопрограмм

Далее давайте разберемся с реализацией сопрограмм.Начиная с Python 3.4, концепция сопрограмм была добавлена ​​в Python, но эта версия сопрограмм по-прежнему основана на объектах-генераторах.В Python 3.5 добавлены async/await., делая реализацию сопрограмм удобнее.

Наиболее часто используемой библиотекой для использования сопрограмм в Python является asyncio, поэтому в этой статье будет представлено использование сопрограмм на основе asyncio.

Сначала нам нужно понять следующие понятия:

  • Event_Loop: цикл события эквивалентна бесконечной петле. Мы можем зарегистрировать некоторые функции на этот цикл события. Когда условия будут выполнены, будет вызываться соответствующий метод обработки.

  • coroutine: китайский перевод называется coroutine, который часто называют типом объекта coroutine в Python.Мы можем зарегистрировать объект coroutine во временном цикле, и он будет вызываться циклом событий. Мы можем использовать ключевое слово async для определения метода, который не будет выполняться немедленно при вызове, а вместо этого возвращает объект сопрограммы.

  • задача: задача, которая является дальнейшей инкапсуляцией объекта сопрограммы, включая различные состояния задачи.

  • будущее: представляет результат задачи, которая выполняется или не выполняется в будущем, по сути, нет существенной разницы от задачи.

Кроме того, нам также необходимо понимать ключевое слово async/await, которое появилось только в Python 3.5 и специально используется для определения сопрограмм. Среди них async определяет сопрограмму, а await используется для приостановки выполнения блокирующих методов.

3.1 Определение сопрограмм

Сначала давайте определим соглашение и почувствуем разницу между ним и обычным процессом в реализации, код выглядит следующим образом:

import asyncio

async def execute(x):
    print('Number:', x)

coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')

loop = asyncio.get_event_loop()
loop.run_until_complete(coroutine)
print('After calling loop')

результат операции:

Coroutine: <coroutine object execute at 0x1034cf830>
After calling execute
Number: 1
After calling loop

Сначала мы представили пакет asyncio, чтобы мы могли использовать async и await, а затем мы использовали async для определения метода execute(), который принимает числовой параметр и печатает число после выполнения метода.

Затем мы вызывали этот метод напрямую, но этот метод не выполнялся, а возвращал объект сопрограммы сопрограммы. Затем мы используем метод get_event_loop() для создания цикла цикла событий и вызываем метод run_until_complete() объекта цикла, чтобы зарегистрировать сопрограмму в цикле цикла событий и запустить ее. Наконец, мы видим, что метод execute() выводит вывод.

Видно, что метод, определенный с помощью async, станет объектом сопрограммы, который не может быть выполнен напрямую, и он должен быть зарегистрирован в цикле событий, прежде чем его можно будет выполнить.

Выше мы также упомянули задачу, которая представляет собой дополнительную инкапсуляцию объекта сопрограммы.По сравнению с объектом сопрограммы он имеет больше рабочих состояний, таких как выполнение, завершение и т. д. Мы можем использовать эти состояния для получения выполнения объекта сопрограммы.

В приведенном выше примере, когда мы передаем объект сопрограммы методу run_until_complete(), он фактически выполняет операцию, которая инкапсулирует сопрограмму в объект задачи.Мы также можем объявить ее явно следующим образом:

import asyncio

async def execute(x):
    print('Number:', x)
    return x

coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')

loop = asyncio.get_event_loop()
task = loop.create_task(coroutine)
print('Task:', task)
loop.run_until_complete(task)
print('Task:', task)
print('After calling loop')

результат операции:

Coroutine: <coroutine object execute at 0x10e0f7830>
After calling execute
Task: <Task pending coro=<execute() running at demo.py:4>>
Number: 1
Task: <Task finished coro=<execute() done, defined at demo.py:4> result=1>
After calling loop

Здесь мы определяем объект цикла, а затем вызываем его метод create_task() для преобразования объекта сопрограммы в объект задачи, затем распечатываем вывод и обнаруживаем, что он находится в состоянии ожидания. Затем мы добавляем объект задачи в цикл событий для выполнения, а затем распечатываем объект задачи и обнаруживаем, что его статус стал завершенным, и мы также можем видеть, что его результатом стал 1, то есть выполнение, которое мы определили. ) возвращает результат.

Кроме того, есть еще один способ определить объект задачи, то есть напрямую через метод asyncio гарантировать_будущее(), возвращаемый результат также является объектом задачи, поэтому мы можем определить его без помощи цикла, даже если у нас есть не объявленный цикл, мы можем заранее определить объект задачи, записав его следующим образом:

import asyncio

async def execute(x):
    print('Number:', x)
    return x

coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')

task = asyncio.ensure_future(coroutine)
print('Task:', task)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:', task)
print('After calling loop')

результат операции:

Coroutine: <coroutine object execute at 0x10aa33830>
After calling execute
Task: <Task pending coro=<execute() running at demo.py:4>>
Number: 1
Task: <Task finished coro=<execute() done, defined at demo.py:4> result=1>
After calling loop

Оказалось, что эффект тот же.

3.2 Привязка обратных вызовов

Кроме того, мы также можем привязать метод обратного вызова к задаче, см. следующий пример:

import asyncio
import requests

async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status

def callback(task):
    print('Status:', task.result())

coroutine = request()
task = asyncio.ensure_future(coroutine)
task.add_done_callback(callback)
print('Task:', task)

loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:', task)

Здесь мы определяем метод request(), запрашиваем Baidu и возвращаем код состояния, но в этом методе нет инструкции print(). Затем мы определяем метод callback(), который получает параметр, являющийся объектом задачи, а затем вызывает метод print() для печати результата объекта задачи. Таким образом, мы определили объект сопрограммы и метод обратного вызова.Теперь мы хотим выполнить объявленный метод callback() после выполнения объекта сопрограммы.

Так как они оба связывают это? Очень просто, просто позвоните в методе Add_done_Callback (), мы вызовут метод обратного вызова () для доставки объектов упакованных задач, такие как выполнение задач, можно вызвать после завершения обратного вызова (), и в то же время в качестве объекта задач параметров также Передача в метод Callback (), вызовите метод результата объекта задачи (), может получить возвращенные результаты.

результат операции:

Task: <Task pending coro=<request() running at demo.py:5> cb=[callback() at demo.py:11]>
Status: <Response [200]>
Task: <Task finished coro=<request() done, defined at demo.py:5> result=<Response [200]>>

Фактически, без метода обратного вызова вы можете напрямую вызвать метод result(), чтобы получить результат сразу после завершения задачи, как показано ниже:

import asyncio
import requests

async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status

coroutine = request()
task = asyncio.ensure_future(coroutine)
print('Task:', task)

loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:', task)
print('Task Result:', task.result())

Результат запуска тот же:

Task: <Task pending coro=<request() running at demo.py:4>>
Task: <Task finished coro=<request() done, defined at demo.py:4> result=<Response [200]>>
Task Result: <Response [200]>

3.3 Многозадачные сопрограммы

В приведенном выше примере мы выполнили только один запрос. Что, если мы хотим выполнить несколько запросов? Мы можем определить список задач, а затем использовать метод asyncio wait() для выполнения, см. следующий пример:

import asyncio
import requests

async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status

tasks = [asyncio.ensure_future(request()) for _ in range(5)]
print('Tasks:', tasks)

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

for task in tasks:
    print('Task Result:', task.result())

Здесь мы используем цикл for для создания пяти задач, формируем список, а затем сначала передаем список методу wait() asyncio, а затем регистрируем его во временном цикле для запуска пяти задач. Наконец, мы выводим текущие результаты задачи, и текущие результаты выглядят следующим образом:

Tasks: [<Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>]
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>

Видно, что пять задач выполнены последовательно и получили результаты.

3.4 Реализация Coroutine

Как я уже говорил, это асинхронность, сопрограмма, задача и обратный вызов, но я не вижу преимуществ сопрограмм? Наоборот, метод написания еще более странный и хлопотный, не волнуйтесь, вышеописанный случай — это просто подготовка к дальнейшему использованию, а теперь давайте официально рассмотрим преимущества сопрограмм в решении задач с интенсивным вводом-выводом!

В приведенном выше коде мы используем в качестве примера сетевой запрос, который является трудоемкой операцией, поскольку после запроса веб-страницы нам нужно дождаться ответа страницы и вернуть результат. Операции, требующие времени ожидания, обычно представляют собой операции ввода-вывода, такие как чтение файлов, сетевые запросы и т. д. Сопрограммы имеют большие преимущества для обработки такого рода операций.При возникновении ситуации, требующей ожидания, программа может временно приостановить работу и вместо этого выполнять другие операции, чтобы избежать ожидания программы и тратить слишком много времени, чтобы полностью использовать ресурсы. .

Чтобы показать преимущества сопрограмм, нам нужно сначала создать подходящую экспериментальную среду. Лучший способ — смоделировать веб-страницу, которая должна ждать определенный период времени, чтобы получить возвращенные результаты. Baidu используется в приведенном выше коде. , но ответ Baidu слишком Он быстрый, и скорость ответа также будет зависеть от скорости локальной сети, поэтому лучший способ — смоделировать медленный сервер локально.Здесь мы выбираем Flask.

Если Flask не установлен, вы можете установить его, выполнив следующую команду:

pip3 install flask

Затем напишите код сервера следующим образом:

from flask import Flask
import time

app = Flask(__name__)

@app.route('/')
def index():
    time.sleep(3)
    return 'Hello!'

if __name__ == '__main__':
    app.run(threaded=True)

Здесь мы определяем сервис Flask. Основной записью является метод index(). Метод вызывает метод sleep() для сна в течение 3 секунд, а затем возвращает результат. То есть для каждого требуется не менее 3 секунд. запрос к этому интерфейсу, поэтому мы моделируем медленный сервисный интерфейс.

Обратите внимание, что когда начнется сервис, метод запуска () добавляет резьбу параметра, что указывает на то, что Flask запускает многопоточный режим, в противном случае по умолчанию только один поток. Если вы не включите режим многопотативного режима, вы можете только обрабатывать несколько запросов одновременно, так что даже если мы используем наше уравнение асинхронной, запрашивающей эту услугу, вы можете только ждать ожидания очереди, то узкое место появится на сервере. Поэтому для открытия необходимо открыть режим многопотативного режима.

После запуска Flask по умолчанию должен работать на 127.0.0.1: 5000. После запуска вывод консоли выглядит следующим образом:

 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Затем мы используем описанный выше метод для повторного запроса:

import asyncio
import requests
import time

start = time.time()

async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    response = requests.get(url)
    print('Get response from', url, 'Result:', response.text)

tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

end = time.time()
print('Cost time:', end - start)

Здесь мы по-прежнему создаем пять задач, а затем передаем список задач в метод wait() и регистрируем его для выполнения во временном цикле.

Результаты приведены ниже:

Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Cost time: 15.049368143081665

Можно обнаружить, что он ничем не отличается от обычного запроса, и он все еще выполняется последовательно, занимает 15 секунд, а в среднем запрос занимает 3 секунды, а как насчет асинхронной обработки?

На самом деле, для достижения асинхронной обработки мы должны сначала иметь операцию приостановки.Когда задаче нужно дождаться результата ввода-вывода, мы можем приостановить текущую задачу и вместо этого выполнить другие задачи, чтобы мы могли полностью использовать ресурсы. вышеуказанный метод. Все они серьезно относятся к последовательному обходу, даже не зависают, как можно добиться асинхронности? слишком много думать.

Для достижения асинхронности Далее мы смотрим на использование ожидания, использование ожидания может быть длительной операцией, ожидающей ожидания, поэтому контроль. Когда ожидаются встречи с выполнением сопрограммы, время цикла будет приостанавливать эту сопрограмму, переходить к реализации другой сопрограммы до тех пор, пока другая сопрограмма не приостановится или не будет выполнена.

Итак, мы можем изменить метод request() в нашем коде, чтобы он выглядел так:

async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    response = await requests.get(url)
    print('Get response from', url, 'Result:', response.text)

Просто добавьте ожидание перед запросами, но выполните следующий код, вы получите следующую ошибку:

Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Cost time: 15.048935890197754
Task exception was never retrieved
future: <Task finished coro=<request() done, defined at demo.py:7> exception=TypeError("object Response can't be used in 'await' expression",)>
Traceback (most recent call last):
  File "demo.py", line 10, in request
    status = await requests.get(url)
TypeError: object Response can't be used in 'await' expression

На этот раз он столкнулся с методом await, который действительно завис и ждал, но в итоге выдал такую ​​ошибку.Эта ошибка означает, что возвращаемый запросами объект Response нельзя использовать с await.Почему? Потому что, согласно официальной документации, объект ожидания должен быть в одном из следующих форматов:

  • Нативный объект сопрограммы, возвращаемый из нативной функции сопрограммы, собственный объект сопрограммы.

  • Объект сопрограммы на основе генератора, возвращаемый функцией, украшенной с помощью types.coroutine() , генератор, украшенный с помощью types.coroutine(), который возвращает объект сопрограммы.

  • An object with an Метод await__ возвращает итератор, содержащий __awaitИтератор, возвращаемый объектом метода.

См.: https://www.python.org/dev/peps/pep-0492/#await-expression.

Ответ, возвращаемый запросами, не соответствует ни одному из вышеуказанных условий, поэтому будет сообщено об указанной выше ошибке.

Затем некоторые друзья обнаружили, что, поскольку за ожиданием может следовать объект сопрограммы, могу ли я использовать асинхронность, чтобы изменить запрошенный метод на объект сопрограммы? Поэтому перепишите его следующим образом:

import asyncio
import requests
import time

start = time.time()

async def get(url):
    return requests.get(url)

async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    response = await get(url)
    print('Get response from', url, 'Result:', response.text)

tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

end = time.time()
print('Cost time:', end - start)

Здесь мы отделяем метод запроса страницы и украшаем его асинхронностью, таким образом мы получаем объект корутины, запускаем его и видим:

Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Cost time: 15.134317874908447

Тем не менее нет, он не выполняется асинхронно, а это означает, что мы не можем инкапсулировать код, включающий операции ввода-вывода, только в асинхронные декорированные методы! Мы должны использовать метод запроса, который поддерживает асинхронные операции для достижения настоящей асинхронности, поэтому aiohttp пригодится здесь.

3.5 Использование aiohttp

AioHTTP - это библиотека поддержки для асинхронных запросов, и использовать ее с нашим Asyncio, может легко реализовать операцию асинхронного запроса.

Способ установки следующий:

pip3 install aiohttp

Ссылка на официальную документацию: https://aiohttp.readthedocs.io/, которая разделена на две части: клиентская и серверная.Подробности см. в официальной документации.

Далее мы будем использовать aiohttp и изменим код на следующий:

import asyncio
import aiohttp
import time

start = time.time()

async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    session.close()
    return result

async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    result = await get(url)
    print('Get response from', url, 'Result:', result)

tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

end = time.time()
print('Cost time:', end - start)

Здесь мы меняем библиотеку запросов с request на aiohttp и делаем запросы через метод get() класса ClientSession aiohttp Результаты следующие:

Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Cost time: 3.0199508666992188

Это сработало! Мы обнаружили, что время, необходимое для этого запроса, изменилось с 15 секунд до 3 секунд, и время сразу стало 1/5 от исходного.

В коде мы используем await, за которым следует метод get().При выполнении этих пяти сопрограмм, если встречается ожидание, текущая сопрограмма будет приостановлена, а другие сопрограммы будут выполняться до тех пор, пока другие сопрограммы также не будут приостановлены или завершены, и затем выполняется следующая сопрограмма.

Когда он начинает запустить, век-цикл будет запустить первую задачу. Для первой задачи выполняется метод GET (), а затем первый await, он приостановлен, но первый шаг этого GET () метода выполнения Неблокируется, он проснулся сразу после приостановления, поэтому он немедленно входит в исполнение, создает объект CLECTICSESSICS, затем сталкивается со второй ждут, вызывает метод запроса Session.get (), а затем приостановлен, потому что запрос принимает Долгое время, так что оно не было пробуждено, поэтому первая задача приостановлена, так что я должен делать дальше? Структура событий будет искать в данный момент необъясненный COROTINE, чтобы продолжить выполнение, поэтому он будет поворачиваться к выполнению второй задачи, что является тем же работой процесса, до тех пор, пока не выполняется метод SESSIT.GET (). Метод пятой задачи, все задачи приостановленный. Все задачи были приостановлены, что мне делать? Пришлось ждать. Через 3 секунды несколько запросов были отреагированы почти одновременно, а затем несколько задач были также пробуждены и выполнены и выполнены, выводя результаты запроса, и последнее время было 3 секунды!

Как насчет этого? В этом удобство асинхронной работы.При обнаружении мешающей операции задача приостанавливается, программа подключается к другим задачам, а не тупо, поэтому вы можете в полной мере использовать время процессора, не тратя время на ожидание ввода-вывода.

Некоторые люди скажут, что в этом случае, в приведенном выше примере, после выдачи сетевого запроса, поскольку следующие 3 секунды ждут, в течение 3 секунд количество задач, которые может обработать ЦП, намного больше, чем это, Так что Разве мы не ставим 10, 20, 50, 100 и 1000 задач для выполнения вместе и, наконец, получаем все результаты примерно за 3 секунды? Потому что все эти задачи ждут вместе после приостановки.

Теоретически это так, но есть предпосылка, что сервер может гарантировать нормальный результат возврата, принимая неограниченное количество запросов одновременно, то есть сервер бесконечно устойчив к давлению, а задержку передачи IO нужно игнорировать, что действительно может быть сделано.Бесконечные задачи выполняются вместе и дают результаты в ожидаемое время.

Здесь мы устанавливаем количество задач на 100 и пробуем снова:

tasks = [asyncio.ensure_future(request()) for _ in range(100)]

Результаты, требующие времени, следующие:

Cost time: 3.106252670288086

Окончательное время работы также составляет около 3 секунд, конечно, дополнительное время — это задержка ввода-вывода.

Видно, что после использования асинхронного боя мы можем выполнить сотни сетевых запросов за одно и то же время, и это использование в сканере, и улучшение скорости можно описать, Это очень значительно.

3.6 Сравнение с однопроцессорным и многопроцессорным

Некоторые из вас могут захотеть узнать, сколько времени потребуется для использования одного процесса и нескольких процессов, если в приведенном выше примере 100 запросов, если вы не используете асинхронные сопрограммы, давайте проверим:

Во-первых, давайте проверим время одного процесса:

import requests
import time

start = time.time()

def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    result = requests.get(url).text
    print('Get response from', url, 'Result:', result)

for _ in range(100):
    request()

end = time.time()
print('Cost time:', end - start)

Последний раз:

Cost time: 305.16639709472656

Затем мы используем многопроцессорность для тестирования, используя библиотеку многопроцессорности:

import requests
import time
import multiprocessing

start = time.time()

def request(_):
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    result = requests.get(url).text
    print('Get response from', url, 'Result:', result)

cpu_count = multiprocessing.cpu_count()
print('Cpu count:', cpu_count)
pool = multiprocessing.Pool(cpu_count)
pool.map(request, range(100))

end = time.time()
print('Cost time:', end - start)

Здесь я использую многопроцессор внутри класса бассейна, который обрабатывает пул. Номер процессора вашего компьютера 8, размер бассейна, где процесс 8.

часы работы:

Cost time: 48.17306900024414

Видно, что многопроцессорная обработка может значительно повысить эффективность по сравнению с однопоточной.

3.7 Объединение с многопроцессорной обработкой

Поскольку и асинхронные сопрограммы, и мультипроцессы улучшают сетевые запросы, почему бы не объединить их? На последнем PyCon 2018 Джон Риз из Facebook представил соответствующие характеристики асинхронности и многопроцессорности, а также разработал новую библиотеку под названием aiomultiprocess.Если интересно, о ней можно узнать: https://www.youtube.com/watch? v=0kXaLh8Fz3k.

Способ установки этой библиотеки:

pip3 install aiomultiprocess

Для использования требуется Python 3.6 и выше.

Используя эту библиотеку, мы можем переписать приведенный выше пример следующим образом:

import asyncio
import aiohttp
import time
from aiomultiprocess import Pool

start = time.time()

async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    session.close()
    return result

async def request():
    url = 'http://127.0.0.1:5000'
    urls = [url for _ in range(100)]
    async with Pool() as pool:
        result = await pool.map(get, urls)
        return result

coroutine = request()
task = asyncio.ensure_future(coroutine)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)

end = time.time()
print('Cost time:', end - start)

Таким образом, для запросов одновременно будут использоваться несколько процессов и асинхронных сопрограмм, конечно, конечный результат на самом деле похож на асинхронный:

Cost time: 3.1156570434570312

Из-за моего тестового интерфейса самый быстрый ответ также составляет 3 секунды, поэтому эта часть дополнительного времени в основном представляет собой задержку передачи ввода-вывода. Но в реальной ситуации ситуации, с которыми мы сталкиваемся при сканировании, постоянно меняются.С одной стороны, мы используем асинхронные сопрограммы для предотвращения блокировки, а с другой стороны, мы используем многопроцессорность, чтобы воспользоваться преимуществами многоядерного ускорения. экономия времени на самом деле весьма значительна.

Выше приведено основное использование сопрограмм в Python, надеюсь, оно вам поможет.

4. Справочные источники

  • http://python.jobbole.com/87310/

  • https://www.cnblogs.com/xybaby/p/6406191.html

  • http://python.jobbole.com/88291/

  • http://lotabout.me/2017/understand-python-asyncio/

  • https://segmentfault.com/a/1190000008814676

  • https://www.cnblogs.com/animalize/p/4738941.html