Вступ до асинхронного програмування на Python

Alex Alex 14 липня 2020
Вступ до асинхронного програмування на Python

Асинхронне програмування – це вид паралельного програмування, в якому якась одиниця роботи може виконуватися окремо від основного потоку виконання програми. Коли робота завершується, основний потік отримує повідомлення про завершення робочого потоку або про помилку. У такого підходу є безліч переваг, таких як підвищення продуктивності застосунків та підвищення швидкості відгуку.

Вступ до асинхронного програмування на PythonЗа останні кілька років асинхронне програмування привернула до себе пильну увагу, і на те є причини. Попри те, що цей вид програмування може бути складніше традиційного послідовного виконання, він набагато ефективніший.

Наприклад, замість того, щоб чекати завершення HTTP-запиту перед продовженням виконання, ви можете відправити запит і виконувати іншу роботу, яка чекає своєї черги, за допомогою асинхронних співпрограм в Python.

Асинхронність – це одна з основних причин популярності вибору Node.js для реалізації бекенду. Велика кількість коду, який ми пишемо, особливо в застосунках з важким введенням-виведенням, такому як на вебсайтах, залежить від зовнішніх ресурсів. В ньому може виявитися все, що завгодно, від віддаленого виклику бази даних до POST-запитів в REST-сервіс. Як тільки ви відправите запит в один з цих ресурсів, ваш код буде просто чекати відповіді. З асинхронним програмуванням ви дозволяєте своєму коду обробляти інші завдання, поки чекаєте відповіді від ресурсів.

Як Python примудряється робити кілька речей одночасно?

Вступ до асинхронного програмування на Python1. Множинні процеси

Найочевидніший спосіб – це використання декількох процесів. З терміналу ви можете запустити свій скрипт два, три, чотири, десять разів, і всі скрипти будуть виконуватися незалежно й одночасно. Операційна система сама подбає про розподіл ресурсів процесора між усіма екземплярами. В якості альтернативи ви можете скористатися бібліотекою multiprocessing, яка вміє породжувати кілька процесів, як показано в прикладі нижче.

from multiprocessing import Process

def print_func(continent='Asia'):
    print('The name of continent is : ', continent)

if __name__ == "__main__":  # confirms that the code is under main function
    names = ['America', 'Europe', 'Africa']
    procs = []
    proc = Process(target=print_func)  # instantiating without any argument
    procs.append(proc)
    proc.start()

    # instantiating process with arguments
    for name in names:
        # print(name)
        proc = Process(target=print_func, args=(name,))
        procs.append(proc)
        proc.start()

    # complete the processes
    for proc in procs:
        proc.join()

Результат:

The name of continent is :  Asia
The name of continent is :  America
The name of continent is :  Europe
The name of continent is :  Africa

2. Множинні потоки

Ще один спосіб запустити кілька робіт паралельно – це використовувати потоки. Потік – це черга виконання, яка дуже схожа на процес, проте в одному процесі ви можете мати кілька потоків, і у всіх них буде спільний доступ до ресурсів. Однак через це написати код потоку буде складно. Аналогічно, важку роботу з виділення пам'яті процесора зробить операційна система, але глобальне блокування інтерпретатора (GIL) дозволить тільки одному потоку Python запускатися в одну одиницю часу, навіть якщо у вас є багатопотоковий код. Так GIL на CPython запобігає багатоядерній конкурентності. Тобто ви насильно можете запуститися тільки на одному ядрі, навіть якщо у вас їх два, чотири або більше.

import threading
 
def print_cube(num):
    """
    function to print cube of given num
    """
    print("Cube: {}".format(num * num * num))
 
def print_square(num):
    """
    function to print square of given num
    """
    print("Square: {}".format(num * num))
 
if __name__ == "__main__":
    # creating thread
    t1 = threading.Thread(target=print_square, args=(10,))
    t2 = threading.Thread(target=print_cube, args=(10,))
 
    # starting thread 1
    t1.start()
    # starting thread 2
    t2.start()
 
    # wait until thread 1 is completely executed
    t1.join()
    # wait until thread 2 is completely executed
    t2.join()
 
    # both threads completely executed
    print("Done!")

Висновок:

Square: 100
Cube: 1000
Done!

3. Співпрограми та yield:

Співпрограми  (coroutine) – це узагальнення підпрограм. Вони використовуються для кооперативної багатозадачності, коли процес добровільно віддає контроль (yield) з якоюсь періодичністю або в періоди очікування, щоб дозволити декільком застосункам працювати одночасно. Співпрограми схожі на генератори, але з додатковими методами і невеликими змінами в тому, як ми використовуємо оператор yield. Генератори виробляють дані для ітерації, в той час як співпрограми можуть ще й споживати дані.

def print_name(prefix):
    print("Searching prefix:{}".format(prefix))
    try : 
        while True:
                # yeild used to create coroutine
                name = (yield)
                if prefix in name:
                    print(name)
    except GeneratorExit:
            print("Closing coroutine!!")
 
corou = print_name("Dear")
corou.__next__()
corou.send("James")
corou.send("Dear James")
corou.close()

Результат роботи:

Searching prefix:Dear
Dear James
Closing coroutine!!

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

Четвертий спосіб – це асинхронне програмування, в якому не бере участь операційна система. З боку операційної системи у вас залишиться один процес, в якому буде всього один потік, але ви все ще зможете виконувати одночасно кілька завдань. Так в чому тут фокус?

Відповідь: asyncio

Asyncio – модуль асинхронного програмування, який був представлений в Python 3.4. Він призначений для використання співпрограм і future для спрощення написання асинхронного коду і робить його майже таким самим читаним, як синхронний код, через відсутності callback-ів.

Asyncio використовує різні конструкції: event loop, співпрограми та future.

  • event loop управляє і контролює виконання різних завдань. Він реєструє їх і обробляє розподіл потоку управління між ними.
  • Співпрограми (про яких ми говорили вище) – це спеціальні функції, робота яких схожа з роботою генераторів в Python, за допомогою await вони повертають потік управління назад в event loop. Запуск співпрограми повинен бути запланований в event loop. Заплановані співпрограми будуть обгорнуті в Завдання, що є типом Future.
  • Future показує результат задачі, яка може або не може бути виконана. Результатом може бути exception.

За допомогою asyncio ви можете структурувати свій код, щоб підзадачі визначалися як співпрограми та дозволяли планувати їх запуск так, як вам заманеться, в тому числі й одночасно. Співпрограми містять точки yield, в яких ми визначаємо можливі точки перемикання контексту. У разі, якщо в черзі очікування є завдання, то контекст буде перемикнуто, в іншому випадку – ні.

Перемикання контексту в asyncio являє собою event loop, який передає потік керування  від однієї співпрограми до іншої.

У наступному прикладі, ми запускаємо 3 асинхронні задачі, які окремо роблять запити до Reddit, витягують і виводять вміст JSON. Ми використовуємо aiohttp – клієнтську бібліотеку http, яка гарантує, що навіть HTTP-запит буде виконаний асинхронно.

import signal  
import sys  
import asyncio  
import aiohttp  
import json

loop = asyncio.get_event_loop()  
client = aiohttp.ClientSession(loop=loop)

async def get_json(client, url):  
    async with client.get(url) as response:
        assert response.status == 200
        return await response.read()

async def get_reddit_top(subreddit, client):  
    data1 = await get_json(client, 'https://www.reddit.com/r/' + subreddit + '/top.json?sort=top&t=day&limit=5')

    j = json.loads(data1.decode('utf-8'))
    for i in j['data']['children']:
        score = i['data']['score']
        title = i['data']['title']
        link = i['data']['url']
        print(str(score) + ': ' + title + ' (' + link + ')')

    print('DONE:', subreddit + '\n')

def signal_handler(signal, frame):  
    loop.stop()
    client.close()
    sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)

asyncio.ensure_future(get_reddit_top('python', client))  
asyncio.ensure_future(get_reddit_top('programming', client))  
asyncio.ensure_future(get_reddit_top('compsci', client))  
loop.run_forever()

Результат роботи коду:

50: Undershoot: Parsing theory in 1965 (http://jeffreykegler.github.io/Ocean-of-Awareness-blog/individual/2018/07/knuth_1965_2.html)
12: Question about best-prefix/failure function/primal match table in kmp algorithm (https://www.reddit.com/r/compsci/comments/8xd3m2/question_about_bestprefixfailure_functionprimal/)
1: Question regarding calculating the probability of failure of a RAID system (https://www.reddit.com/r/compsci/comments/8xbkk2/question_regarding_calculating_the_probability_of/)
DONE: compsci

336: /r/thanosdidnothingwrong -- banning people with python (https://clips.twitch.tv/AstutePluckyCocoaLitty)
175: PythonRobotics: Python sample codes for robotics algorithms (https://atsushisakai.github.io/PythonRobotics/)
23: Python and Flask Tutorial in VS Code (https://code.visualstudio.com/docs/python/tutorial-flask)
17: Started a new blog on Celery - what would you like to read about? (https://www.python-celery.com)
14: A Simple Anomaly Detection Algorithm in Python (https://medium.com/@mathmare_/pyng-a-simple-anomaly-detection-algorithm-2f355d7dc054)
DONE: python

1360: git bundle (https://dev.to/gabeguz/git-bundle-2l5o)
1191: Which hashing algorithm is best for uniqueness and speed? Ian Boyd's answer (top voted) is one of the best comments I've seen on Stackexchange. (https://softwareengineering.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and-speed)
430: ARM launches “Facts” campaign against RISC-V (https://riscv-basics.com/)
244: Choice of search engine on Android nuked by “Anonymous Coward” (2009) (https://android.googlesource.com/platform/packages/apps/GlobalSearch/+/592150ac00086400415afe936d96f04d3be3ba0c)
209: Exploiting freely accessible WhatsApp data or “Why does WhatsApp web know my phone’s battery level?” (https://medium.com/@juan_cortes/exploiting-freely-accessible-whatsapp-data-or-why-does-whatsapp-know-my-battery-level-ddac224041b4)
DONE: programming

Використання Redis і Redis Queue RQ

Використання asyncio та aiohttp не завжди гарна ідея, особливо якщо ви користуєтеся старішими версіями Python. До того ж, бувають моменти, коли вам потрібно розподілити завдання з різних серверів. В цьому випадку можна використовувати RQ (Redis Queue). Це звичайна бібліотека Python для додавання задач в чергу і обробки їх іншими програмами обробки у фоновому режимі. Для організації черги використовується Redis – база даних ключ/значення.

У прикладі нижче ми додали в чергу просту функцію count_words_at_url з допомогою Redis.

from mymodule import count_words_at_url
from redis import Redis
from rq import Queue


q = Queue(connection=Redis())
job = q.enqueue(count_words_at_url, 'http://nvie.com')


******mymodule.py******

import requests

def count_words_at_url(url):
    """Just an example function that's called async."""
    resp = requests.get(url)

    print( len(resp.text.split()))
    return( len(resp.text.split()))

Результат роботи коду:

15:10:45 RQ worker 'rq:worker:EMPID18030.9865' started, version 0.11.0
15:10:45 *** Listening on default...
15:10:45 Cleaning registries for queue: default
15:10:50 default: mymodule.count_words_at_url('http://nvie.com') (a2b7451e-731f-4f31-9232-2b7e3549051f)
322
15:10:51 default: Job OK (a2b7451e-731f-4f31-9232-2b7e3549051f)
15:10:51 Result is kept for 500 seconds

Висновок

В якості прикладу візьмемо шахову виставку, де один з кращих шахістів змагається з великою кількістю людей. У нас є 24 гри і 24 людини, з якими можна зіграти, і, якщо шахіст буде грати з ними синхронно, це займе не менше 12 годин (при умові, що середня гра займає 30 ходів, шахіст продумує хід протягом 5 секунд, а противник – приблизно 55 секунд.) Проте в асинхронному режимі шахіст зможе робити хід і залишати противнику час на роздуми, тим часом переходячи до наступного супротивника і робити хід. Таким чином, зробити хід у всіх 24 іграх можна за 2 хвилини, і виграні всі вони можуть бути лише за одну годину.

Це і мається на увазі, коли говорять про те, що асинхронність прискорює роботу. Про таку швидкість і йде мова. Хороший шахіст не починає грати в шахи швидше, просто час краще оптимізовано, і він не витрачається даремно на очікування. 

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

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

Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.