Функция asyncio.wait(list_task) имеет функционал, схожий с asyncio.gather(). Обе функции асинхронно запускают переданные awaitable-объекты. Для написания парсеров нет большой разницы, какую из этих функций использовать, так как мы будем выполнять только запросы к серверу и сохранение данных в файл.
Так в чём же разница?
asyncio.gather(aws) фокусируется на сборе результатов: она ждёт выполнения переданных в него, aws(awaitable-объектов) и возвращает список полученныx результатов работы для этих объектов. Каждая задача или группа задач может быть отменена методом .cancel(). Для написания парсеров мы этим функционалом пользоваться не будем. Это высокоуровневое решение.
done, pending = asyncio.wait(aws,timeout=2) также ждёт awaitable-объекты(Futures или Tasks), но возвращает два множества (set) из Tasks/Futures: done и pending (выполненные и ожидающие выполнения), соответственно.
Еще одним отличием является то, что в asyncio.wait() может быть использован параметр timeout.
Когда время ожидания сопрограммы истекает, исключение TimeoutError не вызывается. Вместо этого, фьючерсы или задачи, которые не были выполнены в момент тайм-аута, просто возвращаются во второй набор.
Мы можем немного модифицировать код с прошлого этапа и заменить функцию gather() функцией wait().
import asyncio import random import time async def one(): # Получаем текущее время в секундах с начала эпохи. start = time.time() await asyncio.sleep((sleep_time := random.randint(1, 3))) # Получаем имя текущей задачи и выводим сообщение с временем ее выполнения: print(f'{asyncio.current_task().get_name()} ({sleep_time=}) выполнена за {time.time() - start}') async def main(): # Создание списка задач. lst_tasks = [] for x in range(10): # Корутины должны быть явно обернуты в Task. task = asyncio.create_task(one(), name=f'Задача_{x}') lst_tasks.append(task) done, pending = await asyncio.wait(lst_tasks, timeout=2) print(f'Не успели выполниться: {[task.get_name() for task in pending]}') # Даем время выполниться оставшимся задачам await asyncio.sleep(3) asyncio.run(main())
Вывод:
Задача_1 (sleep_time=1) выполнена за 1.012650489807129 Задача_5 (sleep_time=1) выполнена за 1.012650489807129 Задача_3 (sleep_time=1) выполнена за 1.012650489807129 Задача_8 (sleep_time=1) выполнена за 1.012650489807129 Задача_6 (sleep_time=1) выполнена за 1.012650489807129 Не успели выполниться: ['Задача_7', 'Задача_4', 'Задача_0', 'Задача_2', 'Задача_9'] Задача_4 (sleep_time=2) выполнена за 2.0007762908935547 Задача_9 (sleep_time=2) выполнена за 2.0007762908935547 Задача_7 (sleep_time=2) выполнена за 2.0007762908935547 Задача_2 (sleep_time=3) выполнена за 3.010802984237671 Задача_0 (sleep_time=3) выполнена за 3.010802984237671
Обратите внимание, что те задачи, которые попадают в множество pending(ожидающие выполнения) продолжают свое выполнение после срабатывания таймаута.
P.S. При выполнении этого кода вы можете столкнуться с ситуацией, когда одна или несколько задач с sleep_time=2 окажется в множестве done (туда попадет всё, что успеет выполниться в пределах таймаута), а другая (другие) с тем же sleep_time=2 — в pending.
. . . Задача_6 (sleep_time=1) выполнена за 1.0119240283966064 Задача_7 (sleep_time=1) выполнена за 1.0119240283966064 Задача_5 (sleep_time=2) выполнена за 2.025589942932129 Не успели выполниться: ['Задача_8', 'Задача_0', 'Задача_2'] Задача_2 (sleep_time=2) выполнена за 2.025589942932129 Задача_0 (sleep_time=3) выполнена за 3.0177180767059326 . . .
Таким образом, фактическое время выполнения задач и момент, когда истекает таймаут, определяют, в какое множество (done или pending) попадет каждая задача. То, что задачи с одинаковым sleep_time могут попадать в разные списки, связано с тем, что время их старта может незначительно отличаться из-за асинхронной природы выполнения, а также из-за внутренних задержек и точности таймера таймаута.