Асинхронное выполнение кода в Python и Django

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

Разные варианты, описанные ниже, подходят для разных случаев. Например, использование threading не всегда допустимо при работе с Django, в то время как в небольших скриптах нецелесообразно использование связки celery и redis.

Зачем нужен асинхронный подход?

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

Или пример прямо из жизни. Пользователь загружает объемный файл через веб-интерфейс (используется Django), обработка которого на стороне сервера может занять до 3 минут. Естественно, что во view, которая обрабатывает запрос пользователя, обрабатывать файл нельзя — это заблокирует весь поток. В таких случаях и помогает асинхронный подход.

Асинхронный код с помощью threading

Этот вариант — самый простой, и, вероятно, не всегда применимый и правильный. Заключается он в использовании модуля threading.

Код ниже вызывает функцию test_long асинхронно.

def test_long(filepath):
    time.sleep(5)
    f = open(filepath, 'wb+')
    f.write('breakingcode')
    f.close()
    time.sleep(5)

def main():
    threading.Thread(target=test_long, args=(filepath)).start()
    print('Hi!')

main()

В частности, такой подход можно использовать и в Django. Он будет работать, если поток асинхронной функции не использует объекты, уничтожаемые Django после отправки ответа (response).

Асинхронное программирование с Tornado

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

Давайте посмотрим на примеры, взятые отсюда:

import time
from tornado.ioloop import IOLoop
from tornado import gen
 
def my_function(callback):
  print('do some work')
  # Эта строчка блокирует выполнение
  time.sleep(1)
  callback(123)
 
@gen.engine
def f():
  print('start')
  # Вызов my_function и возврат результата, как только вызовется "callback"
  # Результат - это аргумент, передаваемый в функцию обратного вызова(callback)
  result = yield gen.Task(my_function)
  print('result is', result)
  IOLoop.instance().stop()
 
if __name__ == "__main__":
  f()
  IOLoop.instance().start()

Не смотря на корректный вызов my_function, все равно возникнет блокировка. Это результат использования time.sleep(1), который заблокирует весь сервер. Правильный вариант будет выглядеть так:

import time
from tornado.ioloop import IOLoop
from tornado import gen

@gen.engine
def f():
  print('sleeping')
  yield gen.Task(IOLoop.instance().add_timeout, time.time() + 1)
  print('awake!')

if __name__ == "__main__":
  # Обратите внимание, что код выполняется "одновременно"
  IOLoop.instance().add_callback(f)
  IOLoop.instance().add_callback(f)
  IOLoop.instance().start()

По использованию с Django есть примеры.

Использование celery в Django

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

В качестве брокера задач используют redis. Это база данных, которая хранит информацию в виде «ключ-значение». Отличается высокой производительностью. Хранить можно строки, списки, множества, хеш-таблицы.

Отличный мануал по подключению есть у Алексея здесь. Установка и настройка по мануалу труда не составляет.