Глава 9. Введение в асинхронное программирование

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

В данной главе будут охвачены такие темы:

  • Основные понятия асинхронного программирования

  • Основные ключевые отличия между асинхронным программированием и прочими моделями программирования

Технические требования

Вот перечень предварительных требований для данной главы:

  • Убедитесь что на вашем компьютере уже установлен Python 3

  • Вам следует иметь установленными OpenCV и NumPy для вашего дистрибутива Python 3

  • Выгрузите необходимый репозиторий из GitHub

  • На протяжении данной главы мы будем работать с вложенной папкой, имеющей название Chapter09, поэтому проверьте что она у вас уже имеется

  • Ознакомьтесь со следующими видеоматериалами Code in Action

Аналогия на скорую руку

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

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

  • Некую закуску, которая потребует 2 минут на подготовку и 3 минут приготовления/ ожидания

  • Основное блюдо, занимающее 5 минут подготовительных работ и 10 минут готовки/ ожидания

  • Десерт, отнимающий 3 минут на предварительные мероприятия и 5 минут изготовления, ожидания

Теперь рассмотрим тот порядок,в котором данное угощение завершает готовку, ваша цель состоит в том, чтобы сготовить угощение из трёх блюд и это заняло бы наименьшее время. Например, если мы начнём своё поварское дело последовательно, первой мы заверши закуску, что займёт у нас 5 минут, затем мы перейдём к основному блюду, которое потребует 15 минут и, наконец, настанет время для десерта, что поглотит ещё 8 минут, соответственно. В целом нам потребуется на завершение 28 минут.

Основной момент для поиска самого быстрого способа пройти всё это заключается в перекрытии времени готовки/ ожидания одного блюда с временем подготовительных работ для другого. Так как вы не будете заняты пока дожидаетесь окончания готовки уже предварительно замешанного блюда, это время можно сберечь занимаясь подготовкой продуктов для следующего яства. Например, при помощи следующих шагов можно достичь улучшения:

  • Подготовка закуски: 2 минуты.

  • Подготовка вашего основного блюда в то время пока готовится закуска: 5 минут. Приготовление закуски завершится за этом этапе.

  • Подготовка и приготовление десерта пока вы дожидаетесь завершения готовки основного блюда: 8 минут. Ваш десерт завершит приготовление на этом шаге, а вашему основному блюду потребуется ещё 2 минуты до окончательной готовности.

  • Ожидание готовности основного блюда: 2 минуты. На данном этапе вы достигните готовности своего основного блюда.

Выполняя перекрытия времён подготовки блюд, мы сберегаем значительную продолжительность времени готовки угощения из трёх блюд, которое теперь у нас занимает в целом только 17 минут в сравнении с 28 минутами, которые мы тратим при последовательном кухарстве. Тем не менее, очевидно, что существует более одного способа для того чтобы решить какое из блюд начинать готовить первым, а которое из них будет вторым и так далее. Приведём к примеру, иной порядок готовки:

  • Подготовка основного блюда: 5 минуты.

  • Подготовка закуски в то время пока готовится основное блюдо: 2 минут. Вашему основному блюду остаётся готовиться ещё 8 минут.

  • Подготовка десерта пока вы дожидаетесь завершения готовки закуски и пока продолжает готовку основное блюдо: 3 минуты. Ваша закуска завершит приготовление на этом шаге, а вашему основному блюду потребуется ещё 5 минуты до окончательной готовности.

  • Ожидание готовности основного блюда и десерта: 5 минуты. И ваше основное блюдо, и десерт сготовятся на данном этапе.

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

Сопоставление асинхронности с прочими моделями программирования

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

Сопоставление асинхронного и синхронного программирования

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

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

Лучшим подходом является асинхронное программирование, при котором ваш клиент волен продолжать работу, а когда вернутся от его сервера запрошенные данные, именно тогда этот клиент получит уведомление и продолжит обрабатывать свои данные. Асинхронное программирование настолько распространено при веб разработке, что целая модель программирования, именуемая AJAX (сокращение от Asynchronous JavaScript and XML) теперь применяется почти повсеместно на вебсайтах. Кроме того, если вы уже пользовались распространёнными библиотеками JavaScript, такими как jQuery или Node.js, имеются все шансы, что вы уже работали, или по крайней мере слышали соответствующий термин обратного вызова (callback), который всего лишь обозначает некую функцию, которая передаётся в другую функцию для исполнения позднее в соответствующем будущем. Переключение взад и вперёд между исполнением функций является самой основной идеей асинхронного программирования и мы в действительности проанализируем некий расширенный пример использования обратного вызова в Главе 18, Построение сервера с нуля.

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

 

Рисунок 9-1


Различия между синхронными и асинхронными HTTP запросами

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

Сопоставление асинхронного программирования с потоками и множеством процессов

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

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

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

Пример на Python

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

Давайте рассмотрим свой файл Chapter09/example1.py:


# Chapter09/example1.py

from math import sqrt

def is_prime(x):
    print('Processing %i...' % x)

    if x < 2:
        print('%i is not a prime number.' % x)

    elif x == 2:
        print('%i is a prime number.' % x)

    elif x % 2 == 0:
        print('%i is not a prime number.' % x)

    else:
        limit = int(sqrt(x)) + 1
        for i in range(3, limit, 2):
            if x % i == 0:
                print('%i is not a prime number.' % x)
                return

        print('%i is a prime number.' % x)

if __name__ == '__main__':

    is_prime(9637529763296797)
    is_prime(427920331)
    is_prime(157)
 	   

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

Когда вы исполните этот сценарий, ваш вывод должен быть похож на такое:


> python example1.py
Processing 9637529763296797...
9637529763296797 is a prime number.
Processing 427920331...
427920331 is a prime number.
Processing 157...
157 is a prime number.
		

Вероятно вы заметили, что данная программа потребовала значительного времени для обработки самого первого входного данного. Из- за того, каким образом реализована наша функция is_prime(), если сам ввод претендента на простое число значителен, тогда это потребует от is_prime() большего времени на обработку. Таким образом, поскольку у нас имеется большое простое число в качестве данного на входе в первый раз, наша программа на Python подвиснет на существенный промежуток времени прежде чем выведет на печать свои выходные данные. Это, как правило, создаёт для нашей программы ощущение плохой отзывчивости, что является нежелательным как в отношении инженерии программного обеспечения, так и при веб- разработке.

Для улучшения способности к отклику данной программы мы воспользовались преимуществами модуля asyncio, которые были реализованы в нашем файле Chapter09/example2.py:


# Chapter09/example2.py

from math import sqrt

import asyncio

async def is_prime(x):
    print('Processing %i...' % x)

    if x < 2:
        print('%i is not a prime number.' % x)

    elif x == 2:
        print('%i is a prime number.' % x)

    elif x % 2 == 0:
        print('%i is not a prime number.' % x)

    else:
        limit = int(sqrt(x)) + 1
        for i in range(3, limit, 2):
            if x % i == 0:
                print('%i is not a prime number.' % x)
                return
            elif i % 100000 == 1:
                #print('Here!')
                await asyncio.sleep(0)

        print('%i is a prime number.' % x)

async def main():

    task1 = loop.create_task(is_prime(9637529763296797))
    task2 = loop.create_task(is_prime(427920331))
    task3 = loop.create_task(is_prime(157))

    await asyncio.wait([task1, task2, task3])

if __name__ == '__main__':
    try:
        loop = asyncio.get_event_loop()
        loop.run_until_complete(main())
    except Exception as e:
        print('There was a problem:')
        print(str(e))
    finally:
        loop.close()
 	   

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


> python example2.py
Processing 9637529763296797...
Processing 427920331...
427920331 is a prime number.
Processing 157...
157 is a prime number.
9637529763296797 is a prime number.
		

В частности, в то время как обрабатывалось 9637529763296797 (самое большое число во входных данных), наша программа приняла решение переключиться на ввод для последующих вводов данных. Тем самым, результаты для 427920331 и 157 были возвращены ранее, что тем самым улучшило представление программы в её отзывчивости.

Выводы

Асинхронное программирование является моделью программирования, которая основывается на координации задач посредством переключения задач. Она отличается от обычного последовательного (или иначе: синхронного) программирования так как она создаёт перемежение между временами обработки и ожидания, которое предоставляет потенциальное улучшение в скорости. Асинхронное программирование также отличается от потоков и множества процессов, так как оно имеет место только в рамках одного потока в одном отдельном процессе.

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

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

Вопросы

  • Какая основная мысль стоит за асинхронным программированием?

  • Чем асинхронным программирование отличается от синхронного программирования?

  • В чём состоят отличия асинхронного программирования от потоков и многопроцессности?

Дальнейшее чтение

Для получения дополнительных сведений вы можете воспользоваться следующими ссылками: