Что лучше использовать для отправки писем: многопоточность или асинхронность в Python
Что проверяют
Понимаете ли вы, что отправка письма это ожидание сети, а не счёт, и видите ли, что настоящая задача не в выборе между потоками и корутинами, а в надёжности рассылки.
Как отвечать
Сказать, что оба варианта рабочие, потому что время уходит на ожидание ответа сервера, и разложить по условиям:
- потоки:
ThreadPoolExecutorна небольшое число воркеров и обычныйsmtplib. Ничего переписывать не надо, подходит для десятков и сотен писем; - корутины:
aiosmtplibвнутриasyncio.gather. Дешевле по памяти, и выигрыш виден, когда соединений тысячи; - но упирается всё не в Python, а в лимиты почтового провайдера: одновременные соединения, письма в секунду и в сутки. Тысяча потоков не ускорит отправку, она получит отказ и испортит репутацию отправителя;
- поэтому правильный ответ шире вопроса: отправка выносится из веб-запроса в очередь задач с повторами и ограничением скорости, а внутри воркера подойдёт любой из двух подходов. Соединение с SMTP переиспользуется, а не открывается заново на каждое письмо.
Где спотыкаются
Зовут блокирующий smtplib внутри корутины. Цикл событий встаёт на всё время сетевого обмена, и асинхронность превращается в последовательную отправку. Если библиотека синхронная, её место в asyncio.to_thread.
Второе: нет повторов и защиты от дублей. Сбой на середине рассылки заставляет перезапустить задачу, и часть адресатов получает письмо второй раз. Нужен признак отправки в базе и ключ идемпотентности на письмо.
Третье: создают поток на каждое письмо. Тысяча потоков это тысяча стеков и постоянное переключение контекста; нужен пул фиксированного размера, а при асинхронном варианте семафор на число одновременных соединений.