Асинхронне програмування в python

Асинхронне програмування на Python стає все більш популярним. Для цих цілей існує безліч різних бібліотек. Найпопулярніша з них - Asyncio. яка є стандартною бібліотекою Python 3.4. З цієї статті ви дізнаєтеся, що таке асинхронне програмування і чим відрізняються різні бібліотеки, реалізують асинхронність в Python.

По черзі

У кожній програмі рядки коду виконуються по черзі. Наприклад, якщо у вас є рядок коду, яка запитує щось з сервера, то це означає, що ваша програма не робить нічого під час очікування відповіді. У деяких випадках це допустимо, але в багатьох - немає. Одним з рішень цієї проблеми є потоки (threads).

Потоки дають можливість вашій програмі виконувати ряд завдань одночасно. Звичайно, у потоків є ряд недоліків. Багатопотокові програми є більш складними і, як правило, більш схильні до помилок. Вони включають в себе такі проблеми: стан гонки (race condition), взаємна (deadlock) і активна (livelock) блокування, вичерпання ресурсів (resource starvation).

перемикання контексту

Хоча асинхронне програмування і дозволяє обійти проблемні місця потоків, воно було розроблено для зовсім іншої мети - для перемикання контексту процесора. Коли у вас є кілька потоків, кожне ядро ​​процесора може запускати тільки один потік за раз. Для того, щоб все потоки / процеси могли спільно використовувати ресурси, процесор дуже часто перемикає контекст. Щоб спростити роботу, процесор з довільною періодичністю зберігає всю контекстну інформацію потоку і переключається на інший потік.

Асинхронне програмування - це потокова обробка програмного забезпечення / користувальницького простору, де додаток, а не процесор, управляє потоками і перемиканням контексту. В асинхронному програмуванні контекст перемикається тільки в заданих точках перемикання, а не з періодичністю, визначеною CPU.

ефективний секретар

Потоки - це п'ять секретарів, у кожного з яких по одному завданню, але тільки одному з них дозволено працювати в певний момент часу. Для того, щоб секретарі працювали в потоковому режимі, необхідно пристрій, який контролює їх роботу, але нічого не розуміє в самих завданнях. Оскільки пристрій не розуміє характер завдань, воно постійно перемикалася б між п'ятьма секретарями, навіть якщо троє з них сидять, нічого не роблячи. Близько 57% (трохи менше, ніж 3/5) перемикання контексту були б марні. Незважаючи на те, що перемикання контексту процесора є неймовірно швидким, воно все одно забирає час і ресурси процесора.

зелені потоки

Як бачите, API-інтерфейс Gevent виглядає так само, як і потоки. Однак за кадром він використовує співпрограми (coroutines), а не потоки, і запускає їх в циклі подій (event loop) для постановки в чергу. Це означає, що ви отримуєте переваги потоків, без розуміння співпрограми, але ви не позбавляєтеся від проблем, пов'язаних з потоками. Gevent - хороша бібліотека, але тільки для тих, хто розуміє, як працюють потоки.

Давайте розглянемо деякі аспекти асинхронного програмування. Один з таких аспектів - це цикл подій. Цикл подій - це черга подій / завдань і цикл, який витягує завдання з черги і запускає їх. Ці завдання називаються співпрограмами. Вони являють собою невеликий набір команд, що містять, крім іншого, інструкції про те, які події при необхідності потрібно повертати в чергу.

Функція зворотного виклику (callback)

В Python багато бібліотек для асинхронного програмування, найбільш популярними є Tornado, Asyncio і Gevent. Давайте подивимося, як працює Tornado. Він використовує стиль зворотного виклику (callbacks) для асинхронного мережного введення-виведення. Як передзвонити - це функція, яка означає: «Як тільки це буде зроблено, виконайте цю функцію». Іншими словами, ви телефонуєте в службу підтримки і залишаєте свій номер, щоб вони, коли будуть доступні, передзвонили, замість того, щоб чекати їх відповіді.
Давайте подивимося, як зробити те ж саме, що і вище, використовуючи Tornado:

tornado. ioloop. IOLoop. instance (). start ()

У прикладі ви можете помітити, що перший рядок функції handle_response перевіряє наявність помилки. Це необхідно, тому що неможливо обробити виняток. Якщо виняток було створено, то воно не буде відпрацьовуватися в коді через циклу подій. Коли fetch виконується, він запускає HTTP-запит, а потім обробляє відповідь в циклі подій. До того моменту, коли виникне помилка, стек викликів буде містити тільки цикл подій та поточну функцію, при цьому ніде в коді не спрацює виняток. Таким чином, будь-які винятки, створені в функції зворотного виклику, переривають цикл подій та зупиняють виконання програми. Тому всі помилки повинні бути передані як об'єкти, а не оброблені у вигляді винятків. Це означає, що якщо ви не перевірили наявність помилок, то вони не будуть оброблятися.
Інша проблема із зворотними викликами полягає в тому, що в асинхронному програмуванні єдиний спосіб уникати блокувань - це зворотний виклик. Це може привести до дуже довгому ланцюжку: зворотний виклик після зворотного виклику після зворотного виклику. Оскільки втрачається доступ до стека і змінним, ви в кінцевому підсумку переносите великі об'єкти в усі ваші зворотні виклики, але якщо ви використовуєте сторонні API-інтерфейси, то не можете передати що-небудь в зворотний виклик, якщо він цього не може прийняти. Це також стає проблемою, тому що кожен зворотний виклик діє як потік. Наприклад, ви хотіли б викликати три API-інтерфейсу і дочекатися, поки всі три повернуть результат, щоб його узагальнити. У Gevent ви можете це зробити, але не зі зворотними викликами. Вам доведеться трохи поворожити, зберігаючи результат в глобальній змінній і перевіряючи в зворотному виклику, чи є результат остаточним.

Якщо ви хочете запобігти блокування введення-виведення, ви повинні використовувати або потоки, або асинхронність. В Python ви вибираєте між зеленими потоками і асинхронним зворотним викликом. Ось деякі з їхніх особливостей:

зелені потоки

  • потоки управляються на рівні додатків, а не апаратно;
  • включають в себе всі проблеми потокового програмування.

Як передзвонити

  • співпрограми невидимі для програміста;
  • зворотні виклики обмежують використання винятків;
  • зворотні виклики важко налагоджують.

Як вирішити ці проблеми?

Аж до Python 3.3 зелені потоки і зворотний виклик були оптимальними рішеннями. Щоб перевершити ці рішення, потрібна підтримка на рівні мови. Python повинен якимось чином частково виконати метод, припинити виконання, підтримуючи при цьому об'єкти стека і виключення. Якщо ви знайомі з концепціями Python, то розумієте, що я натякаю на генератори. Генератори дозволяють функції повертати між окремими елементами списку за раз, зупиняючи виконання до того моменту, коли наступний елемент буде запитано. Проблема з генераторами полягає в тому, що вони повністю залежать від функції, що викликає його. Іншими словами, генератор не може викликати генератор. Принаймні так було до тих пір, поки в PEP 380 не додали синтаксис yield from. який дозволяє генератору отримати результат іншого генератора. Хоч асинхронність і не є головним призначенням генераторів, вони містять весь функціонал, щоб бути досить корисними. Генератори підтримують стек і можуть створювати виключення. Якби ви написали цикл подій, в якому б запускалися генератори, у вас вийшла б відмінна асинхронна бібліотека. Саме так і було створено бібліотеку Asyncio.

Прим. перев. У прикладах використовується aiohttp версії 1.3.5. В останній версії бібліотеки синтаксис інший.

Кілька особливостей, які потрібно відзначити:

  • помилки коректно передаються в стек;
  • можна повернути об'єкт, якщо необхідно;
  • можна запустити всі співпрограми;
  • немає зворотних викликів;
  • рядок 10 не виконається до тих пір, поки рядок 9 не буде повністю виконана.

Єдина проблема полягає в тому, що об'єкт виглядає як генератор, і це може викликати проблеми, якщо насправді це був генератор.

Async і Await

Бібліотека Asyncio досить потужна, тому Python вирішив зробити її стандартної бібліотекою. У синтаксис також додали ключове слово async. Ключові слова призначені для більш чіткого позначення асинхронного коду. Тому тепер методи не плутаються з генераторами. Ключове слово async йде до def. щоб показати, що метод є асинхронним. Ключове слово await показує, що ви очікуєте завершення співпрограми. Ось той же приклад, але з ключовими словами async / await:

Програма складається з методу async. Під час виконання він повертає співпрограми, яка потім знаходиться в очікуванні.

висновок

В Python вбудована відмінна асинхронна бібліотека. Давайте ще раз згадаємо проблеми потоків і подивимося, чи вирішені вони тепер:

  • процесорний переключення контексту. Asyncio є асинхронним і використовує цикл подій. Він дозволяє перемикати контекст програмно;
  • стан гонки: оскільки Asyncio запускає тільки одну співпрограми і переключається тільки в точках, які ви визначаєте, ваш код не схильний до проблеми гонки потоків;
  • взаємна / активна блокування: оскільки тепер немає гонки потоків, то не потрібно турбуватися про блокування. Хоча взаємне блокування все ще може виникнути в ситуації, коли дві співпрограми викликають один одного, це настільки малоймовірно, що вам доведеться постаратися, щоб таке трапилося;
  • вичерпання ресурсів: оскільки співпрограми запускаються в одному потоці і не вимагають додаткової пам'яті, стає набагато складніше вичерпати ресурси. Однак в Asyncio є пул «виконавців» (executors), який по суті є пулом потоків. Якщо запускати занадто багато процесів в пулі виконавців, ви все одно можете зіткнутися з нестачею ресурсів.

Незважаючи на те, що Asyncio досить хороший, у нього є і проблеми. По-перше, Asyncio був доданий в Python недавно. Є деякі недоробки, які ще не виправлені. По-друге, коли ви використовуєте асинхронність, це означає, що весь ваш код повинен бути асинхронним. Це пов'язано з тим, що виконання асинхронних функцій може займати занадто багато часу, тим самим блокуючи цикл подій.

Існує кілька варіантів асинхронного програмування в Python. Ви можете використовувати зелені потоки, зворотні виклики або співпрограми. Хоча варіантів багато, кращий з них - Asyncio. Якщо використовуєте Python 3.5, то вам краще використовувати цю бібліотеку, так як вона вбудована в ядро ​​python.