Как сделать так, чтобы функция, на выполнение которой требуется меньше времени, выдавала результат первой?

Как сделать так, чтобы функция, на выполнение которой требуется меньше времени, выдавала результат первой?

import asyncio
import time

async def main():
    await fun1()
    await fun2()

async def fun1():
    time.sleep(3)       # Предположим, что тут какая-то работа, время
    print("From fun1")  # выполнения которой неизвестно на момент написания кода.
    
async def fun2():
    time.sleep(1)
    print("From fun2")


asyncio.run(main())

Ответы (2 шт):

Автор решения: insolor

Сразу ошибка: для ожидания в асинхронных функциях нужно использовать await asyncio.sleep(), а не time.sleep(). Обычный time.sleep просто блокирует основной поток, ничего другое в это время в этом же потоке не будет выполняться. await asyncio.sleep() отдает управление асинхронном эвент лупу и позволяет во время ожидания выполняться другим асинхронным задачам.

Далее, код

await fun1()
await fun2()

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

sync_fun1()
sync_fun2()

Т.е. в данном случае ожидание завершения функций не происходит параллельно. Чтобы работало параллельно, нужно использовать asyncio.wait или asyncio.gather.

Основная разница:

  • asyncio.gather (буквально "собрать") собирает результаты работы асинхронных функций (или любых awaitable объектов) и возвращает как список.
  • asyncio.wait ожидает завершения работы списка тасков (этот список еще нужно подготовить), причем может ожидать завершения всех тасков, первого завершившегося, первого упавшего с исключением (задается параметром return_when, см. документацию), и возвращает список завершившихся тасков и список еще не завершившихся (эти списки актуальны только если использован параметр return_when со значением не ALL_COMPLETED, тогда второй список будет непустым).

Через asyncio.gather:

import asyncio


async def main():
    await asyncio.gather(fun1(), fun2())


async def fun1():
    await asyncio.sleep(3)       # Предположим, что тут какая-то работа, время
    print("From fun1")  # выполнения которой неизвестно на момент написания кода.


async def fun2():
    await asyncio.sleep(1)
    print("From fun2")


asyncio.run(main())

Через asyncio.wait нужно сначала создать список задач (тасков), потом этот список передать в функцию:

import asyncio


async def main():
    tasks = [asyncio.create_task(fun1()), asyncio.create_task(fun2())]
    await asyncio.wait(tasks)


async def fun1():
    await asyncio.sleep(3)       # Предположим, что тут какая-то работа, время
    print("From fun1")  # выполнения которой неизвестно на момент написания кода.


async def fun2():
    await asyncio.sleep(1)
    print("From fun2")


asyncio.run(main())

Асинхронный - действительно не значит параллельный, асинхронный - это конкурентный. Несколько задач конкурируют за поток. Параллельно они естественно не выполняются, но во время простоя одной задачи (того же await asyncio.sleep или ожидания данных откуда-то, например), может выполняться другая задача. Можно сказать, что тут не выполнение параллельно, а ожидание.

→ Ссылка
Автор решения: Stanislav Volodarskiy

Асинхронный не значит параллельный. Асинхронные задачи исполняются в одной нити, кусочками, последовательно. Когда вы пишите асинхронный код, вы позволяете компилятору переставлять кусочки так чтобы уменьшить простои. Но если ваш код спит четыре четыре секунды или загружает процессор четыре секунды, асинхронность не сделает его быстрее. Нужна параллельность.

Вы можете разбить код на отдельные нити и ваш пример будет выполнятся три секунды:

import time
import concurrent.futures


def fun1():
    time.sleep(3)       # Предположим, что тут какая-то работа, время
    print("From fun1")  # выполнения которой неизвестно на момент написания кода.
    
def fun2():
    time.sleep(1)
    print("From fun2")


with concurrent.futures.ThreadPoolExecutor() as e:
    tasks = [e.submit(t) for t in (fun1, fun2)]
    for t in tasks:
        t.result()

Но есть GIL. Если ваш код действительно четыре секунды работает на процессоре, параллельные нити будут работать четыре секунды - в каждый момент времени GIL разрешает вычислять только одной нити.

Если нужно много вычислять, понадобятся процессы. Они обеспечивают истинную, ничем не ограниченную параллельность:

import multiprocessing
import time


def fun1():
    time.sleep(3)       # Предположим, что тут какая-то работа, время
    print("From fun1")  # выполнения которой неизвестно на момент написания кода.
    

def fun2():
    time.sleep(1)
    print("From fun2")


if __name__ == '__main__':
    procs = [multiprocessing.Process(target=f) for f in (fun1, fun2)]
    for p in procs:
        p.start()
    for p in procs:
        p.join()
→ Ссылка