Я использую Apache JMeter для тестирования крошечного приложения Flask. Приложение выполняет какую-то задачу, связанную с процессором.
Удивительно, но запуск приложения Flask с --without-threads
дает заметно лучшие результаты, чем запуск с --with-threads
. Как это могло быть?
Вот некоторые из настроек Apache JMeter и соответствующие результаты:
Я ожидаю, что в случае задачи, связанной исключительно с процессором, многопоточная версия должна быть по крайней мере такой же быстрой, как и однопоточная. Позволь мне объяснить:
С точки зрения выполнения фактической задачи ЦП, я ожидаю, что обе версии будут работать одинаково. Однако с точки зрения того, как быстро может быть обслужен следующий поток, я ожидаю, что многопоточная версия будет иметь небольшое преимущество, потому что запрос уже был обслужен Flask, и он застрял только в ожидании ЦП.
В однопоточной версии (т. е. --without-threads
) одновременно обслуживается только один запрос, в то время как все остальные запросы ожидают обслуживания Flask. Другими словами, Flask вводит определенные «накладные расходы на обслуживание».
В идеальном мире Flask мог бы мгновенно обслужить новый запрос. Другими словами, накладные расходы Flask, обслуживающего HTTP-запрос, будут равны 0. В этом случае я ожидаю, что однопоточная и многопоточная версии будут одинаково быстрыми, потому что не имеет значения, ожидают ли потоки выполнения. обслуживаться Flask или ожидать доступа к процессору.
Я предполагаю, что мое понимание неверно. Где я не прав?
@ Томас Спасибо за ответ. Не могли бы вы объяснить, как GIL объясняет, что я вижу, если эти «потоки» настоящие? Другими словами, если эти «потоки» являются реальными потоками, не должны ли однопоточные и многопоточные версии быть одинаково производительными?
Нет, потому что все блокировки, разблокировки и переключение контекста создают дополнительные накладные расходы, которых нет в однопоточном сценарии. Потоки Python полезны, когда потоки тратят большую часть своего времени на ожидание операций ввода-вывода, а не на то, что загружает ЦП. Вам нужны отдельные процессы для этого.
Как предложил @Thomas, я провел еще несколько тестов, используя готовый к работе сервер. Я выбрал сервер gunicorn, потому что его легко настроить с помощью Python 3.9.
gunicorn принимает два аргумента командной строки, относящиеся к этой теме:
--workers
- "Количество рабочих процессов для обработки запросов". Значение по умолчанию — 1.--threads
— «Количество рабочих потоков для обработки запросов». Значение по умолчанию также равно 1.Увеличение --workers
до уровня, с которым может справиться мой процессор, действительно улучшило производительность. Увеличение --threads
не произошло. Кроме того, запуск 8 рабочих процессов с 1 потоком дал лучшие результаты, чем запуск 8 рабочих процессов с 4 потоками.
Итак, я попытался смоделировать ввод-вывод, заснув на полсекунды. Наконец, увеличение количества потоков действительно улучшило производительность.
Мое приложение также связано с процессором. Я использовал режим gthread gunicorn от --worker-class=gthread
, и результат лучше, чем в другом режиме. Можешь попробовать.
Дополнительная информация: https://medium.com/building-the-system/gunicorn-3-means-of-concurrency-efbb547674b7
flask run
предназначен только для целей разработки. На производительность не претендует. Вместо этого попробуйте это с реальным сервером WSGI, подходящим для производства. Если эти «потоки» являются реальными потоками (а не процессами), GIL Python объясняет, что вы видите.