4

Die beiden Klassen stellen ausgezeichnete Abstraktionen für die gleichzeitige Programmierung dar, daher ist es ein wenig beunruhigend, dass sie nicht dieselbe API unterstützen.Warum ist asyncio.Future mit concurrent.futures.Future inkompatibel?

Insbesondere nach dem docs:

asyncio.Future ist fast mit concurrent.futures.Future kompatibel.

Unterschiede:

  • result() und exception() Sie ein Timeout Argument nicht nehmen und eine Ausnahme auslösen, wenn die Zukunft noch nicht geschehen ist.
  • Rückrufe, die mit add_done_callback() registriert sind, werden immer über die Ereignisschleife call_soon_threadsafe() aufgerufen.
  • Diese Klasse ist nicht mit den Funktionen wait() und as_completed() im concurrent.futures-Paket kompatibel.

Die obige Liste ist tatsächlich unvollständig, es gibt ein paar Unterschiede:

  • running() Methode fehlt
  • result() und exception()InvalidStateError erhöhen kann, wenn zu früh genannt

Sind diese aufgrund der inhärenten Natur einer Ereignisschleife, die diese Operationen entweder nutzlos oder zu umständlich zu implementieren macht?

Und was ist die Bedeutung des Unterschieds bezogen auf add_done_callback()? Wie auch immer, der Callback wird garantiert zu irgendeiner unbestimmten Zeit nach dem Abschluss der Futures ausgeführt, also ist es nicht vollkommen konsistent zwischen den beiden Klassen?

Antwort

3

concurrent.futures.Future bietet eine Möglichkeit, Ergebnisse zwischen verschiedenen Threads und Prozessen in der Regel zu teilen, wenn Sie Executor verwenden.

asyncio.Future löst die gleiche Aufgabe aber für coroutines, das sind eigentlich einige spezielle Art von Funktionen in der Regel in einem Prozess/Thread asynchron ausgeführt. "Asynchron" im aktuellen Kontext bedeutet, dass die Ereignisschleife den Code verwaltet, der den Fluss dieser Koroutinen ausführt: Er kann die Ausführung innerhalb einer Coroutine aussetzen, eine andere Coroutine ausführen und später zur Ausführung der ersten Coroutine zurückkehren - alles normalerweise in einem Thread/Prozess.

Diese Objekte (und viele andere threading/asyncio Objekte wie Lock, Event, Semaphore etc.) sehen ähnlich aus, weil die Idee der Parallelität im Code mit Threads/Prozesse und Koroutinen ähnlich ist.

Ich denke, der Hauptgrund, warum Objekte anders sind, ist historisch: asyncio wurde viel später als threading und concurrent.futures erstellt. Es ist wahrscheinlich unmöglich, concurrent.futures.Future zu ändern, um mit asyncio zu arbeiten, ohne Klasse API zu brechen.

Sollten beide Klassen in der "idealen Welt" eins sein? Dies ist wahrscheinlich ein strittiges Problem, aber ich sehe viele Nachteile davon: während asyncio und threading auf den ersten Blick ähnlich aussehen, sind sie sehr unterschiedlich in vielerlei Hinsicht, einschließlich der internen Implementierung oder Art des Schreibens asyncio/non-asyncio-Code (siehe async/await Schlüsselwörter).

Ich denke, es ist wahrscheinlich für das Beste, dass Klassen anders sind: Wir teilen klar verschiedene Arten von Nebenläufigkeit (auch wenn ihre Ähnlichkeit auf den ersten Blick seltsam aussieht).

4

Der Hauptgrund für den Unterschied ist, wie Threads (und Prozesse) mit Blöcken umgehen, und wie Coroutinen Ereignisse behandeln, die blockieren. Beim Threading wird der aktuelle Thread angehalten, bis die Bedingung aufgelöst wird und der Thread vorwärts gehen kann. Wenn Sie beispielsweise im Fall von Futures das Ergebnis einer Zukunft anfordern, ist es in Ordnung, den aktuellen Thread zu suspendieren, bis dieses Ergebnis verfügbar ist.

Das Gleichzeitigkeitsmodell einer Ereignisschleife ist jedoch, dass Sie nicht den Code aussetzen, sondern zur Ereignisschleife zurückkehren und erneut aufgerufen werden, wenn Sie bereit sind. Es ist also ein Fehler, das Ergebnis einer asyncio-Zukunft anzufordern, die kein Ergebnis bereithält.

Sie könnten denken, dass die asyncio Zukunft nur warten könnte und während das ineffizient wäre, wäre es wirklich so schlecht für Ihre Coroutine zu blockieren? Es stellt sich jedoch heraus, dass der Coroutinenblock sehr wahrscheinlich bedeutet, dass die Zukunft niemals abgeschlossen wird. Es ist sehr wahrscheinlich, dass das Ergebnis der Zukunft durch einen Code festgelegt wird, der der Ereignisschleife zugeordnet ist, die den Code ausführt, der das Ergebnis anfordert. Wenn der Thread, der diese Ereignisschleife ausführt, blockiert, würde kein Code ausgeführt werden, der der Ereignisschleife zugeordnet ist. Das Blockieren des Ergebnisses würde also zum Deadlock führen und verhindern, dass das Ergebnis produziert wird.

Also, ja, die Unterschiede in der Schnittstelle sind aufgrund dieses inhärenten Unterschieds. Als Beispiel würden Sie keine asyncio future mit der Abstraktion von concurrent.futures waiter verwenden, da dies wiederum den Ereignis-Loop-Thread blockieren würde. Der add_done_callbacks Unterschied garantiert, dass Callbacks in der Ereignisschleife ausgeführt werden. Das ist wünschenswert, weil sie die Thread-lokalen Daten der Ereignisschleife erhalten. Außerdem nimmt viel Coroutine-Code an, dass es nie gleichzeitig mit anderem Code aus derselben Ereignisschleife ausgeführt wird. Das heißt, Koroutinen sind nur Thread-sicher unter der Annahme, dass zwei Koroutinen aus der gleichen Ereignisschleife nicht gleichzeitig laufen. Das Ausführen der Rückrufe in der Ereignisschleife vermeidet viele Probleme mit der Threadsicherheit und erleichtert das Schreiben von korrektem Code.

Verwandte Themen