Відмінність між програмою та майбутнім / завданням у Python 3.5?


100

Скажімо, у нас є фіктивна функція:

async def foo(arg):
    result = await some_remote_call(arg)
    return result.upper()

Яка різниця між:

import asyncio    

coros = []
for i in range(5):
    coros.append(foo(i))

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(coros))

І:

import asyncio

futures = []
for i in range(5):
    futures.append(asyncio.ensure_future(foo(i)))

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(futures))

Примітка . Приклад повертає результат, але це не є питанням фокусу питання. Якщо значення повернення має значення, використовуйте gather()замість wait().

Незалежно від вартості повернення, я шукаю чіткість ensure_future(). wait(coros)і wait(futures)обидва виконують супроводи, то коли і навіщо слід обгортати супровід ensure_future?

В основному, який правильний спосіб (tm) запустити купу неблокуючих операцій за допомогою Python 3.5 async?

Що робити для отримання додаткового кредиту, якщо я хочу групувати дзвінки? Наприклад, мені потрібно зателефонувати some_remote_call(...)1000 разів, але я не хочу руйнувати веб-сервер / базу даних / тощо з 1000 одночасних з'єднань. Це можливо зробити за допомогою потоку чи пулу процесів, але чи можна це зробити asyncio?

Оновлення до 2020 року (Python 3.7+) : не використовуйте ці фрагменти. Замість цього використовуйте:

import asyncio

async def do_something_async():
    tasks = []
    for i in range(5):
        tasks.append(asyncio.create_task(foo(i)))
    await asyncio.gather(*tasks)

def do_something():
    asyncio.run(do_something_async)

Також розгляньте можливість використання Тріо , надійної третьої альтернативи асинціо.

Відповіді:


95

Спрограма - це функція генератора, яка може одночасно отримувати значення та приймати значення зовні. Перевага використання кореневої програми полягає в тому, що ми можемо призупинити виконання функції та відновити її пізніше. У разі роботи з мережею, є сенс призупинити виконання функції, поки ми чекаємо відповіді. Ми можемо використовувати час для запуску деяких інших функцій.

Майбутнє - це як Promiseоб'єкти з Javascript. Це як заповнювач цінності, яка буде матеріалізована в майбутньому. У вищезгаданому випадку під час очікування мережевого вводу-виводу функція може дати нам контейнер, обіцяючи, що він заповнить контейнер значенням, коли операція завершиться. Ми тримаємося за майбутній об’єкт, і коли він виконаний, ми можемо викликати метод на ньому, щоб отримати фактичний результат.

Прямий відповідь: Вам не потрібні, ensure_futureякщо вам не потрібні результати. Вони хороші, якщо вам потрібні результати або вилучення випадків, що відбулися.

Додаткові кредити: я б обрав run_in_executorі передав Executorекземпляр, щоб контролювати кількість максимум робітників.

Пояснення та зразки кодів

У першому прикладі ви використовуєте супровідні програми. waitФункція приймає купу співпрограми і об'єднує їх разом. Так wait()закінчується, коли всі спроби вичерпані (завершено / закінчено повернути всі значення).

loop = get_event_loop() # 
loop.run_until_complete(wait(coros))

run_until_completeМетод буде переконатися , що петля жива , поки виконання не буде завершено. Зверніть увагу, як у цьому випадку ви не отримуєте результатів виконання асинхронізації.

У другому прикладі ви використовуєте ensure_futureфункцію, щоб обернути підпрограму і повернути Taskоб'єкт, який є своєрідним Future. Планується, що програма буде виконана в циклі основної події під час виклику ensure_future. Повернутий об'єкт майбутнього / завдання ще не має значення, але з часом, коли мережеві операції закінчаться, майбутній об'єкт буде зберігати результат операції.

from asyncio import ensure_future

futures = []
for i in range(5):
    futures.append(ensure_future(foo(i)))

loop = get_event_loop()
loop.run_until_complete(wait(futures))

Отже, у цьому прикладі ми робимо те саме, за винятком того, що використовуємо ф'ючерси, а не просто супроводи.

Давайте розглянемо приклад того, як використовувати асинціо / співпраці / ф'ючерси:

import asyncio


async def slow_operation():
    await asyncio.sleep(1)
    return 'Future is done!'


def got_result(future):
    print(future.result())

    # We have result, so let's stop
    loop.stop()


loop = asyncio.get_event_loop()
task = loop.create_task(slow_operation())
task.add_done_callback(got_result)

# We run forever
loop.run_forever()

Тут ми використали create_taskметод на loopоб’єкті. ensure_futureпланував би завдання в циклі основної події. Цей метод дозволяє намітити намічену програму на вибраному циклі.

Ми також бачимо концепцію додавання зворотного дзвінка за допомогою add_done_callbackметоду на об'єкті завдання.

A Task- це doneколи супровід повертає значення, збільшує виняток або скасовується. Існують методи перевірки цих випадків.

Я написав кілька публікацій в блогах на ці теми, які можуть допомогти:

Звичайно, ви можете знайти більш детальну інформацію в офіційному посібнику: https://docs.python.org/3/library/asyncio.html


3
Я оновив своє запитання, щоб бути більш чітким - якщо мені не потрібен результат від супроводу, чи потрібно все-таки використовувати ensure_future()? І якщо мені потрібен результат, чи не можу я просто використати run_until_complete(gather(coros))?
knite

1
ensure_futureпланує виконувати програму в циклі подій. Тому я б сказав так, це потрібно. Але, звичайно, ви можете запланувати процедури, використовуючи й інші функції / методи. Так, ви можете скористатися gather()- але зібрання будуть чекати, поки всі відповіді будуть зібрані.
masnun

5
@AbuAshrafMasnun @knite gatherі waitфактично оберніть задані кореневища як завдання, використовуючи ensure_future(див. Джерела тут і тут ). Тому використовувати ensure_futureзаздалегідь немає сенсу , і це не має нічого спільного з отриманням результатів чи ні.
Вінсент

8
@AbuAshrafMasnun @knite Також ensure_futureє loopаргумент, тому немає ніяких причин використовувати loop.create_taskнад ensure_future. І run_in_executorне буде працювати з супротинами, замість цього слід використовувати семафор .
Вінсент

2
@vincent є причина використовувати create_taskбільше ensure_future, дивіться документи . Цитатаcreate_task() (added in Python 3.7) is the preferable way for spawning new tasks.
masi

24

Проста відповідь

  • Викликаючи функцію coutut ( async def), НЕ виконайте її. Він повертає об'єкти підпрограми, подібно функції генератора повертає об'єкти генератора.
  • await отримує значення з підпрограм, тобто "викликає" підпрограму
  • eusure_future/create_task заплануйте програму для запуску циклу подій на наступну ітерацію (хоча і не чекаючи, коли вони закінчать, як демонова нитка).

Деякі приклади коду

Давайте спочатку очистимо деякі терміни:

  • функція супроводу, та, яку ви використовуєте async def;
  • об'єкт кореневої програми, що ви отримали, коли ви "викликаєте" функцію кореневища;
  • завдання, об'єкт, обгорнутий навколо об'єкта кореневища для запуску в циклі подій.

Справа 1, awaitна супровід

Ми створюємо дві підпрограми, awaitодну, і використовуємо create_taskдля запуску іншої.

import asyncio
import time

# coroutine function
async def p(word):
    print(f'{time.time()} - {word}')


async def main():
    loop = asyncio.get_event_loop()
    coro = p('await')  # coroutine
    task2 = loop.create_task(p('create_task'))  # <- runs in next iteration
    await coro  # <-- run directly
    await task2

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

ви отримаєте результат:

1539486251.7055213 - await
1539486251.7055705 - create_task

Поясніть:

task1 виконується безпосередньо, а task2 виконується в наступній ітерації.

Випадок 2, поступаючись контролю за циклом подій

Якщо ми замінимо основну функцію, ми можемо побачити інший результат:

async def main():
    loop = asyncio.get_event_loop()
    coro = p('await')
    task2 = loop.create_task(p('create_task'))  # scheduled to next iteration
    await asyncio.sleep(1)  # loop got control, and runs task2
    await coro  # run coro
    await task2

ви отримаєте результат:

-> % python coro.py
1539486378.5244057 - create_task
1539486379.5252144 - await  # note the delay

Поясніть:

Під час виклику asyncio.sleep(1)елемент керування повертається до циклу подій, і цикл перевіряє виконання завдань, після чого він виконує завдання, створені create_task.

Зауважте, що ми спочатку викликаємо функцію кореневища, але не awaitїї, тому ми просто створили єдину кореневу програму, а не запускаємо її. Потім ми знову зателефонуємо до функції create_taskкореневища та завернемо його у виклик, create_task насправді запланує виконання програми для наступної ітерації. Отже, в результаті create taskвиконується раніше await.

Насправді, справа тут у тому, щоб повернути цикл контролю, який можна використати, asyncio.sleep(0)щоб побачити той самий результат.

Під капотом

loop.create_taskнасправді дзвінки asyncio.tasks.Task(), які дзвонять loop.call_soon. І loop.call_soonпоставить завдання loop._ready. Під час кожної ітерації циклу він перевіряє наявність усіх зворотних викликів у циклі.

asyncio.wait, asyncio.ensure_futureа asyncio.gatherнасправді дзвонити loop.create_taskпрямо чи опосередковано.

Також зверніть увагу на документи :

Зворотні дзвінки викликаються в тому порядку, в якому вони зареєстровані. Кожен зворотний дзвінок буде викликаний рівно один раз.


1
Дякую за чисте пояснення! Слід сказати, це досить жахливий дизайн. API високого рівня протікає низькорівневою абстракцією, що надмірно ускладнює API.
Борис Бурков

1
перевірити проект curio, який добре розроблений
ospider

Приємне пояснення! Я думаю, ефект await task2дзвінка міг би бути уточнений. В обох прикладах виклик loop.create_task () - це те, що планує task2 у циклі подій. Таким чином, в обох колишніх програмах ви можете видалити await task2і все-таки task2 з часом запуститься. У ex2 поведінка буде ідентичною, оскільки await task2я вважаю, що це просто планування вже виконаної задачі (яка не буде виконуватись вдруге), тоді як у ex1 поведінка буде дещо іншою, оскільки task2 не буде виконана, поки основна не буде завершена. Щоб побачити різницю, додайте print("end of main")наприкінці головного ex1
Андрія

10

Коментар Вінцента посилається на https://github.com/python/asyncio/blob/master/asyncio/tasks.py#L346 , в якому видно, що для вас wait()передбачено супроводи ensure_future()!

Іншими словами, нам потрібне майбутнє, і супроводи будуть мовчки перетворюватися на них.

Цю відповідь я оновлю, коли знайду остаточне пояснення, як створювати сумісні процедури / ф'ючерси.


Чи означає це , що для сопрограммного об'єкта c, await cеквівалентно await create_task(c)?
Олексій

3

З BDFL [2013]

Завдання

  • Це супровід, загорнутий у майбутнє
  • клас Завдання - це підклас класу Майбутнє
  • Так це працює і з очікуванням !

  • Чим вона відрізняється від голої програми?
  • Він може досягти прогресу, не чекаючи цього
    • Поки ви чекаєте чогось іншого, тобто
      • чекати [щось_ельс]

Зважаючи на це, ensure_futureмає сенс як назва створення завдання, оскільки результат майбутнього буде обчислюватися, чи будете ви його чекати чи ні (поки ви чогось чекаєте). Це дозволяє циклу подій виконати завдання, поки ви чекаєте на інші речі. Зауважте, що в Python 3.7 create_taskкращий спосіб забезпечити майбутнє .

Примітка: я змінив "урожай" на слайдах Гідо на "очікування" тут на сучасність.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.