Асинхронное выполнение кода в 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. Это база данных, которая хранит информацию в виде «ключ-значение». Отличается высокой производительностью. Хранить можно строки, списки, множества, хеш-таблицы.
Отличный мануал по подключению есть у Алексея здесь. Установка и настройка по мануалу труда не составляет.