Unterschied zwischen Coroutine und Future / Task in Python 3.5?


99

Nehmen wir an, wir haben eine Dummy-Funktion:

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

Was ist der Unterschied zwischen:

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

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

Und:

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))

Hinweis : Das Beispiel gibt ein Ergebnis zurück, dies ist jedoch nicht der Schwerpunkt der Frage. Wenn der Rückgabewert wichtig ist, verwenden Sie gather()anstelle von wait().

Unabhängig vom Rückgabewert suche ich Klarheit ensure_future(). wait(coros)und wait(futures)beide führen die Coroutinen aus. Wann und warum sollte eine Coroutine eingewickelt werden ensure_future?

Was ist der richtige Weg, um eine Reihe von nicht blockierenden Vorgängen mit Python 3.5 auszuführen async?

Was passiert, wenn ich die Anrufe stapeln möchte? Zum Beispiel muss ich some_remote_call(...)1000 Mal anrufen , aber ich möchte den Webserver / die Datenbank / etc nicht mit 1000 gleichzeitigen Verbindungen zerstören. Dies ist mit einem Thread oder Prozesspool möglich, aber gibt es eine Möglichkeit, dies zu tun asyncio?

Antworten:


94

Eine Coroutine ist eine Generatorfunktion, die sowohl Werte liefern als auch Werte von außen akzeptieren kann. Der Vorteil der Verwendung einer Coroutine besteht darin, dass wir die Ausführung einer Funktion anhalten und später fortsetzen können. Im Falle eines Netzwerkbetriebs ist es sinnvoll, die Ausführung einer Funktion anzuhalten, während wir auf die Antwort warten. Wir können die Zeit nutzen, um einige andere Funktionen auszuführen.

Eine Zukunft ist wie die PromiseObjekte aus Javascript. Es ist wie ein Platzhalter für einen Wert, der in Zukunft materialisiert wird. In dem oben genannten Fall kann uns eine Funktion während des Wartens auf Netzwerk-E / A einen Container geben, ein Versprechen, dass der Container nach Abschluss des Vorgangs mit dem Wert gefüllt wird. Wir halten am zukünftigen Objekt fest und wenn es erfüllt ist, können wir eine Methode aufrufen, um das tatsächliche Ergebnis abzurufen.

Direkte Antwort: Sie brauchen nicht, ensure_futurewenn Sie die Ergebnisse nicht brauchen. Sie sind gut, wenn Sie die Ergebnisse benötigen oder Ausnahmen abrufen.

Zusätzliche Credits: Ich würde run_in_executoreine ExecutorInstanz auswählen und übergeben , um die Anzahl der maximalen Mitarbeiter zu steuern.

Erklärungen und Beispielcodes

Im ersten Beispiel verwenden Sie Coroutinen. Die waitFunktion nimmt eine Reihe von Coroutinen und kombiniert sie miteinander. Wird wait()beendet, wenn alle Coroutinen erschöpft sind (abgeschlossen / beendet, alle Werte zurückgegeben).

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

Die run_until_completeMethode würde sicherstellen, dass die Schleife aktiv ist, bis die Ausführung abgeschlossen ist. Bitte beachten Sie, dass Sie in diesem Fall nicht die Ergebnisse der asynchronen Ausführung erhalten.

Im zweiten Beispiel verwenden Sie die ensure_futureFunktion, um eine Coroutine zu verpacken und ein TaskObjekt zurückzugeben, das eine Art ist Future. Die Coroutine soll beim Aufruf in der Hauptereignisschleife ausgeführt werden ensure_future. Das zurückgegebene Future / Task-Objekt hat noch keinen Wert, aber im Laufe der Zeit, wenn die Netzwerkoperationen beendet sind, enthält das Future-Objekt das Ergebnis der Operation.

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))

In diesem Beispiel machen wir dasselbe, außer dass wir Futures verwenden, anstatt nur Coroutinen zu verwenden.

Schauen wir uns ein Beispiel für die Verwendung von Asyncio / Coroutines / Futures an:

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()

Hier haben wir die create_taskMethode für das loopObjekt verwendet. ensure_futurewürde die Aufgabe in der Hauptereignisschleife planen. Diese Methode ermöglicht es uns, eine Coroutine in einer von uns ausgewählten Schleife zu planen.

Wir sehen auch das Konzept des Hinzufügens eines Rückrufs mithilfe der add_done_callbackMethode für das Task-Objekt.

A Taskist, donewenn die Coroutine einen Wert zurückgibt, eine Ausnahme auslöst oder abgebrochen wird. Es gibt Methoden, um diese Vorfälle zu überprüfen.

Ich habe einige Blog-Beiträge zu diesen Themen geschrieben, die helfen könnten:

Weitere Details finden Sie natürlich im offiziellen Handbuch: https://docs.python.org/3/library/asyncio.html


3
Ich habe meine Frage aktualisiert, um sie etwas klarer zu gestalten. Wenn ich das Ergebnis der Coroutine nicht benötige, muss ich sie trotzdem verwenden ensure_future()? Und wenn ich das Ergebnis brauche, kann ich es nicht einfach verwenden run_until_complete(gather(coros))?
Stricken

1
ensure_futureplant die Ausführung der Coroutine in der Ereignisschleife. Also würde ich ja sagen, es ist erforderlich. Natürlich können Sie die Coroutinen auch mit anderen Funktionen / Methoden planen. Ja, Sie können verwenden gather()- aber sammeln wird warten, bis alle Antworten gesammelt sind.
Masnun

5
@AbuAshrafMasnun @knite gatherund waitverpacken die angegebenen Coroutinen tatsächlich als Aufgaben mit ensure_future(siehe die Quellen hier und hier ). Es macht also keinen Sinn, ensure_futurevorher zu verwenden, und es hat nichts damit zu tun, die Ergebnisse zu erhalten oder nicht.
Vincent

8
@AbuAshrafMasnun Auch @knite, ensure_futureeine hat loopArgument, so gibt es keinen Grund zu verwenden , loop.create_tasküber ensure_future. Und run_in_executorfunktioniert nicht mit Coroutinen, stattdessen sollte ein Semaphor verwendet werden.
Vincent

2
@ Vincent Es gibt einen Grund, create_tasküber zu verwenden ensure_future, siehe Dokumente . Zitatcreate_task() (added in Python 3.7) is the preferable way for spawning new tasks.
masi

24

Einfache Antwort

  • Wenn Sie eine Coroutine-Funktion ( async def) aufrufen, wird sie NICHT ausgeführt. Es gibt Coroutine-Objekte zurück, so wie die Generatorfunktion Generatorobjekte zurückgibt.
  • await Ruft Werte von Coroutinen ab, dh "ruft" die Coroutine auf
  • eusure_future/create_task Planen Sie die Coroutine so, dass sie bei der nächsten Iteration in der Ereignisschleife ausgeführt wird (obwohl Sie nicht darauf warten, dass sie beendet wird, wie bei einem Daemon-Thread).

Einige Codebeispiele

Lassen Sie uns zunächst einige Begriffe klären:

  • Coroutine-Funktion, die Sie sind async def;
  • Coroutine-Objekt, was Sie erhalten haben, wenn Sie eine Coroutine-Funktion "aufrufen";
  • Task, ein Objekt, das um ein Coroutine-Objekt gewickelt ist, um in der Ereignisschleife ausgeführt zu werden.

Fall 1 awaitauf einer Coroutine

Wir erstellen zwei Coroutinen, awaiteine, und verwenden sie create_task, um die andere auszuführen.

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())

Sie erhalten Ergebnis:

1539486251.7055213 - await
1539486251.7055705 - create_task

Erklären:

Task1 wurde direkt ausgeführt, und Task2 wurde in der folgenden Iteration ausgeführt.

Fall 2, der die Kontrolle über die Ereignisschleife ergibt

Wenn wir die Hauptfunktion ersetzen, sehen wir ein anderes Ergebnis:

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

Sie erhalten Ergebnis:

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

Erklären:

Beim Aufruf asyncio.sleep(1)wurde das Steuerelement an die Ereignisschleife zurückgegeben, und die Schleife prüft, ob Aufgaben ausgeführt werden sollen, und führt dann die von erstellte Aufgabe aus create_task.

Beachten Sie, dass wir zuerst die Coroutine-Funktion aufrufen, aber nicht await, also haben wir nur eine einzelne Coroutine erstellt und sie nicht zum Laufen gebracht. Dann rufen wir die Coroutine-Funktion erneut auf und schließen sie in einen create_taskAufruf ein. Creat_task plant tatsächlich, dass die Coroutine bei der nächsten Iteration ausgeführt wird. Also, im Ergebnis create taskwird vorher ausgeführt await.

Eigentlich geht es hier darum, der Schleife die Kontrolle zurückzugeben, mit der Sie asyncio.sleep(0)das gleiche Ergebnis sehen können.

Unter der Haube

loop.create_taskruft tatsächlich an asyncio.tasks.Task(), was anrufen wird loop.call_soon. Und loop.call_soonwird die Aufgabe in setzen loop._ready. Während jeder Iteration der Schleife wird nach allen Rückrufen in loop._ready gesucht und ausgeführt.

asyncio.wait, asyncio.ensure_futureUnd asyncio.gatherrufen tatsächlich loop.create_taskdirekt oder indirekt.

Beachten Sie auch in den Dokumenten :

Rückrufe werden in der Reihenfolge aufgerufen, in der sie registriert sind. Jeder Rückruf wird genau einmal aufgerufen.


1
Danke für eine saubere Erklärung! Ich muss sagen, es ist ein ziemlich schreckliches Design. Bei einer API auf hoher Ebene tritt eine Abstraktion auf niedriger Ebene auf, wodurch die API zu kompliziert wird.
Boris Burkov

1

Schöne Erklärung! Ich denke, die Wirkung des await task2Anrufs könnte geklärt werden. In beiden Beispielen ist der Aufruf von loop.create_task () das, was task2 in der Ereignisschleife plant. Also kannst du in beiden Exs die löschen await task2und trotzdem wird task2 irgendwann ausgeführt. In Beispiel 2 ist das Verhalten identisch, da await task2ich glaube, dass nur die bereits abgeschlossene Aufgabe geplant wird (die nicht ein zweites Mal ausgeführt wird), während in Beispiel 1 das Verhalten geringfügig anders sein wird, da Aufgabe 2 erst ausgeführt wird, wenn main abgeschlossen ist. Um den Unterschied zu sehen, fügen Sie print("end of main")am Ende von ex1's main hinzu
Andrew

10

Ein Kommentar von Vincent, der mit https://github.com/python/asyncio/blob/master/asyncio/tasks.py#L346 verlinkt ist und zeigt, dass wait()die Coroutinen ensure_future()für Sie eingepackt sind !

Mit anderen Worten, wir brauchen eine Zukunft, und Coroutinen werden stillschweigend in sie umgewandelt.

Ich werde diese Antwort aktualisieren, wenn ich eine endgültige Erklärung zum Batching von Coroutinen / Futures finde.


Bedeutet es , dass für ein Koroutine Objekt c, await centspricht await create_task(c)?
Alexey

3

Aus der BDFL [2013]

Aufgaben

  • Es ist eine Coroutine in einer Zukunft
  • class Task ist eine Unterklasse der Klasse Future
  • So funktioniert es auch mit Warten !

  • Wie unterscheidet es sich von einer bloßen Coroutine?
  • Es kann Fortschritte machen, ohne darauf zu warten
    • Solange Sie auf etwas anderes warten, dh
      • warte auf [etwas anderes]

In diesem Sinne ensure_futureist es sinnvoll, einen Namen für die Erstellung einer Aufgabe zu erstellen, da das Ergebnis der Zukunft berechnet wird, unabhängig davon, ob Sie darauf warten oder nicht (solange Sie auf etwas warten). Auf diese Weise kann die Ereignisschleife Ihre Aufgabe abschließen, während Sie auf andere Dinge warten. Beachten Sie, dass in Python 3.7 create_taskder bevorzugte Weg ist, um eine Zukunft zu sichern .

Hinweis: Ich habe "Ertrag von" in Guidos Folien geändert, um hier auf die Moderne zu "warten".

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.