Всем привет! Сегодня мы погрузимся во внутренности asyncio. Мы узнаем о цикле событий и механизмах уровня ОС, которые позволяют работать asyncio.

Редакция

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

Допустим, у нас есть такая функция. Обратите внимание, это часть псевдокода:

def fetch_and_process_a_long_text():
  res = []
  url = 'https://example.com/some_long_text'
  content = fetch_text(url)     # we want to exit the thread here
  res = content.split()
  return res

Этот псевдокод извлекает длинный текст, а затем разбивает его на слова. Используя многопоточность, мы могли бы выполнять эту функцию в отдельном потоке. Сначала мы запустим наш основной поток, а затем переключимся на поток, который выполняет эту функцию. Когда поток «доходит до fetch_text(url)», он «выходит» из потока и продолжает выполнение основного потока. Когда мы закончим извлечение текста и будем готовы продолжить выполнение дочернего потока, он переключится обратно на поток, выполняющий функцию «fetch_and_process_a_long_text», и продолжит работу с «content = fetch_text(url )” до возврата результата. Затем закрываем нить.

Что такое цикл событий?

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

Ядром цикла событий является цикл while, который перебирает все задачи, назначенные asyncio, и выполняет их одну за другой. Когда он сталкивается с операцией ввода-вывода, вместо выполнения самой операции он делегирует операцию операционной системе, выполняя системный вызов. Если в строке, содержащей операцию ввода-вывода, есть ключевое слово «ожидание», выполняется выход из текущей задачи и переход к следующей.

Цикл событий также постоянно отслеживает завершенные операции ввода-вывода, вызывая системный вызов «select». Этот вызов возвращает список завершенных операций, которые затем выполняются путем перебора ожидающих выполнения задач и выполнения кода, следующего за ключевым словом «ожидание». Вот моя собственная псевдокодовая реализация цикла событий:

class EventLoop():
  def __init__(self, tasks_list):
    self.tasks_list = tasks_list
    self.pending_tasks = []

  def start_event_loop():
    while True:
      if not tasks_list and not self.pending_tasks:  
        # if both pending and tasks lists are empty -> stop event loop
        break
      
      if tasks_list:
        # take next tasks
        # remove from task_list
        # run until 'await'
        # deligate I/O task to the OS with self.run_await
        # add to pending_list
        # when the result is ready, 
        #      make_select_system_call will show it
        current_task = self.tasks_list.pop()
        run_until_await(current_task)
        self.run_await(current_task) 
        self.pending_tasks.append(current_task)
      
      # get completed I/O tasks
      # iterate trough them and run the code that comes after 'await'
      finished_tasks = make_select_system_call()
      for finished_task in finished_tasks:
         run_after_await(finished_task)
         self.pending_tasks.remove(task)      
        
  def run_await(self, task):
    return make_system_call_to_run_io_task(task)


async def fetch_and_process_a_long_text():
  res = []
  url = 'https://example.com/some_long_text'
  await content = fetch_text(url)
  res = content.split()
  return res


if __name__ == '__main__':
  tasks = [fetch_and_process_a_long_text]
  event_loop = EventLoop(tasks)
  event_loop.start_event_loop()

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

В этом примере метод «fetch_and_process_a_long_text» является сопрограммой. Сопрограммы — это методы, которые можно вызывать асинхронно. Чтобы включить asyncio, Python добавил два ключевых слова: "async" и "await". Ключевое слово "async" указывает, что метод является сопрограммой, а ключевое слово "await" сообщает циклу обработки событий, где находится операция ввода-вывода.

Таким образом, цикл событий позволяет эффективно выполнять асинхронные операции ввода-вывода в одном потоке, делегируя операции ввода-вывода операционной системе и отслеживая, какие операции завершены. Корутины и ключевые слова «async» и «await» используются для включения asyncio в Python.

Что такое системный вызов select?

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

Не существует единого единого вызова «выбрать». На самом деле существует множество типов селекторов, которые могут обеспечить дополнительные преимущества. Хотя Python предоставляет вам селектор по умолчанию, какой из них выбрать, зависит от проекта и ОС.

В Python 3.7 и более ранних версиях asyncio предоставляет следующие селекторы:

  • DefaultSelector: селектор по умолчанию для текущей платформы.
  • SelectSelector: селектор на основе функции select().
  • EpollSelector: селектор, основанный на системном вызове epoll() (только для Linux).
  • KqueueSelector: селектор, основанный на системном вызове kqueue() (FreeBSD, macOS и другие Unix-подобные системы).

Вы можете импортировать их следующим образом:

from selectors import SelectSelector, EpollSelector, DefaultSelector, KqueueSelector

Начиная с Python 3.8, asyncio представляет новую реализацию селектора для конкретной ОС, называемую «proactor», которая доступна только в Windows:

  • ProactorEventLoop: селектор на основе портов завершения ввода-вывода, доступный только в Windows.

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

Вот как установить селектор для asyncio:

from selectors import DefaultSelector
selector = DefaultSelector()
loop = asyncio.SelectorEventLoop(selector)
asyncio.set_event_loop(loop)

Примечание. Если вы запускаете новый поток и используете там asyncio, запустите приведенный выше код с селектором, который вы использовали в основном потоке.

УФ-петля

Когда вы пишете производственный код, лучше использовать uvloop, чем встроенный цикл обработки событий asyncio.

Uvloop — это быстрая и удобная замена встроенному асинхронному циклу обработки событий Python. Он написан на Cython и основан на libuv, многоплатформенной библиотеке поддержки с упором на асинхронный ввод-вывод. uvloop совместим с существующим асинхронным кодом, а также предлагает значительные улучшения производительности.

Чтобы использовать uvloop, вы можете установить его с помощью pip:

pip install uvloop

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

import uvloop
import asyncio
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

После этого все последующие вызовы asyncio будут использовать реализацию uvloop вместо реализации по умолчанию.

Краткое содержание

В статье объясняется, как можно эффективно выполнять асинхронные операции ввода-вывода в одном потоке с помощью цикла обработки событий.

  • Цикл событий — это механизм, состоящий из очереди, в которой хранятся задачи, подлежащие выполнению, механизма выполнения этих задач и механизма отслеживания завершенных операций ввода-вывода.
  • Системный вызов select позволяет программе отслеживать несколько файловых дескрипторов и определять, какие из них готовы для чтения или записи.
  • Корутины и ключевые слова «async» и «await» используются для включения asyncio в Python.
  • Если вы создаете несколько потоков и используете asyncio с этими потоками, используйте тот же селектор в дочерних потоках, что и в основном потоке.
  • Используйте uvloop в производственной среде.

Использованная литература:

https://www.youtube.com/watch?v=E7Yn5biBZ58&list=PLhNSoGM2ik6SIkVGXWBwerucXjgP1rHmB&index=2

https://docs.python.org/3/library/asyncio-eventloop.html