|
| 1 | +.. currentmodule:: trio_asyncio |
| 2 | + |
| 3 | +++++++++++++ |
| 4 | + Principles |
| 5 | +++++++++++++ |
| 6 | + |
| 7 | +-------------------------------------- |
| 8 | + Async function "flavors" |
| 9 | +-------------------------------------- |
| 10 | + |
| 11 | +As you might recall from the discussion of async "sandwiches" in the |
| 12 | +`Trio tutorial |
| 13 | +<https://trio.readthedocs.io/en/stable/tutorial.html#async-sandwich>`__, |
| 14 | +every async function ultimately must do its useful work by directly or |
| 15 | +indirectly calling back into the same async library (such as asyncio |
| 16 | +or Trio) that's managing the currently running event loop. If a |
| 17 | +function invoked within a :func:`trio.run` calls |
| 18 | +:func:`asyncio.sleep`, or a function invoked within an |
| 19 | +:func:`asyncio.run` calls :func:`trio.sleep`, the sleep function will |
| 20 | +send a message to the event loop that the event loop doesn't know how |
| 21 | +to handle, and some sort of error will result. |
| 22 | + |
| 23 | +In a program that uses trio-asyncio, you probably have some async |
| 24 | +functions implemented in terms of Trio calls and some implemented in |
| 25 | +terms of asyncio calls. In order to keep track of which is which, |
| 26 | +we'll call these "Trio-flavored" and "asyncio-flavored" functions, |
| 27 | +respectively. It is critical that you understand which functions in |
| 28 | +your program are Trio-flavored and which are asyncio-flavored, just |
| 29 | +like it's critical that you understand which functions are synchronous |
| 30 | +and which ones are async. Unfortunately, there's no syntactic marker |
| 31 | +for flavor: both Trio-flavored and asyncio-flavored functions are |
| 32 | +defined with ``async def fn()`` and call other async functions with |
| 33 | +``await other_fn()``. You'll have to keep track of it some other |
| 34 | +way. To help you out, every function in trio-asyncio documents its |
| 35 | +flavor, and we recommend that you follow this convention in your own |
| 36 | +programs too. |
| 37 | + |
| 38 | +The general rules that determine flavor are as follows: |
| 39 | + |
| 40 | +* Every async function in the ``trio`` module is Trio-flavored. |
| 41 | + Every async function in the ``asyncio`` module is asyncio-flavored. |
| 42 | + |
| 43 | +* Flavor is transitive: if async function ``foo()`` calls ``await |
| 44 | + bar()``, then ``foo()`` has ``bar()``'s flavor. (If ``foo()`` |
| 45 | + calls ``await baz()`` too, then ``bar()`` and ``baz()`` had better |
| 46 | + have the same flavor.) |
| 47 | + |
| 48 | +* trio-asyncio gives you the ability to call functions whose flavor is |
| 49 | + different than your own, but you must be explicit about it. |
| 50 | + :func:`trio_asyncio.aio_as_trio` takes an asyncio-flavored function |
| 51 | + and returns a Trio-flavored wrapper for it; |
| 52 | + :func:`trio_as_aio` takes a Trio-flavored function and |
| 53 | + returns an asyncio-flavored wrapper for it. |
| 54 | + |
| 55 | +If you don't keep track of your function flavors correctly, you might |
| 56 | +get exceptions like the following: |
| 57 | + |
| 58 | +* If you call a Trio function where an asyncio function is expected: ``RuntimeError: |
| 59 | + Task got bad yield:`` followed by either ``WaitTaskRescheduled(abort_func=...)`` |
| 60 | + or ``<class 'trio._core._traps.CancelShieldedCheckpoint'>`` |
| 61 | + |
| 62 | +* If you call an asyncio function where a Trio function is expected: ``TypeError: |
| 63 | + trio.run received unrecognized yield message <Future ...>.`` |
| 64 | + |
| 65 | +Other errors are possible too. |
| 66 | + |
| 67 | +----------------------- |
| 68 | + Flavor versus context |
| 69 | +----------------------- |
| 70 | + |
| 71 | +The concept of function flavor is distinct from the concept of |
| 72 | +"asyncio context" or "Trio context". You're in Trio context if you're |
| 73 | +(indirectly) inside a call to :func:`trio.run`. You're in asyncio |
| 74 | +context if :func:`asyncio.get_running_loop` returns a valid event |
| 75 | +loop. In a trio-asyncio program, you will frequently be in both Trio |
| 76 | +context and asyncio context at the same time, but each async function is |
| 77 | +either Trio-flavored or asyncio-flavored (not both). |
| 78 | + |
| 79 | +Most *synchronous* asyncio or Trio functions (:meth:`trio.Event.set`, |
| 80 | +:meth:`asyncio.StreamWriter.close`, etc) only require you to be in |
| 81 | +asyncio or Trio context, and work equally well regardless of the |
| 82 | +flavor of function calling them. The exceptions are functions that |
| 83 | +access the current task (:func:`asyncio.current_task`, |
| 84 | +:func:`trio.hazmat.current_task`, and anything that calls them), |
| 85 | +because there's only a meaningful concept of the current *foo* task |
| 86 | +when a *foo*-flavored function is executing. For example, this means |
| 87 | +context managers that set a timeout on their body (``with |
| 88 | +async_timeout.timeout(N):``, ``with trio.move_on_after(N):``) must be |
| 89 | +run from within the correct flavor of function. |
| 90 | + |
| 91 | +--------------------------------- |
| 92 | + Flavor transitions are explicit |
| 93 | +--------------------------------- |
| 94 | + |
| 95 | +As mentioned above, trio-asyncio does not generally allow you to |
| 96 | +transparently call ``await trio.something()`` from asyncio code, nor |
| 97 | +vice versa; you need to use :func:`aio_as_trio` or |
| 98 | +:func:`trio_as_aio` when calling a function whose flavor is |
| 99 | +different than yours. This is certainly more frustrating than having |
| 100 | +it "just work". Unfortunately, semantic differences between Trio and |
| 101 | +asyncio (such as how to signal cancellation) need to be resolved at |
| 102 | +each boundary between asyncio and Trio, and we haven't found a way to |
| 103 | +do this with acceptable performance and robustness unless those |
| 104 | +boundaries are marked. |
| 105 | + |
| 106 | +If you insist on living on the wild side, trio-asyncio does provide |
| 107 | +:func:`allow_asyncio` which allows *limited, |
| 108 | +experimental, and slow* mixing of Trio-flavored and asyncio-flavored |
| 109 | +calls in the same Trio-flavored function. |
| 110 | + |
| 111 | +------------------------------------------- |
| 112 | + trio-asyncio's place in the asyncio stack |
| 113 | +------------------------------------------- |
| 114 | + |
| 115 | +At its base, asyncio doesn't know anything about futures or |
| 116 | +coroutines, nor does it have any concept of a task. All of these |
| 117 | +features are built on top of the simpler interfaces provided by the |
| 118 | +event loop. The event loop itself has little functionality beyond executing |
| 119 | +synchronous functions submitted with :meth:`~asyncio.loop.call_soon` |
| 120 | +and :meth:`~asyncio.loop.call_later` |
| 121 | +and invoking I/O availability callbacks registered using |
| 122 | +:meth:`~asyncio.loop.add_reader` and :meth:`~asyncio.loop.add_writer` |
| 123 | +at the appropriate times. |
| 124 | + |
| 125 | +trio-asyncio provides an asyncio event loop implementation which |
| 126 | +performs these basic operations using Trio APIs. Everything else in |
| 127 | +asyncio (futures, tasks, cancellation, and so on) is ultimately |
| 128 | +implemented in terms of calls to event loop methods, and thus works |
| 129 | +"magically" with Trio once the trio-asyncio event loop is |
| 130 | +installed. This strategy provides a high level of compatibility with |
| 131 | +asyncio libraries, but it also means that asyncio-flavored code |
| 132 | +running under trio-asyncio doesn't benefit much from Trio's more |
| 133 | +structured approach to concurrent programming: cancellation, |
| 134 | +causality, and exception propagation in asyncio-flavored code are just |
| 135 | +as error-prone under trio-asyncio as they are under the default |
| 136 | +asyncio event loop. (Of course, your Trio-flavored code will still |
| 137 | +benefit from all the usual Trio guarantees.) |
| 138 | + |
| 139 | +If you look at a Trio task tree, you'll see only one Trio task for the |
| 140 | +entire asyncio event loop. The distinctions between different asyncio |
| 141 | +tasks are erased, because they've all been merged into a single pot of |
| 142 | +callback soup by the time they get to trio-asyncio. Similarly, context |
| 143 | +variables will only work properly in asyncio-flavored code when |
| 144 | +running Python 3.7 or later (where they're supported natively), even |
| 145 | +though Trio supports them on earlier Pythons using a backport package. |
| 146 | + |
| 147 | +---------------------------- |
| 148 | + Event loop implementations |
| 149 | +---------------------------- |
| 150 | + |
| 151 | +An asyncio event loop may generally be interrupted and restarted at any |
| 152 | +time, simply by making repeated calls to :meth:`run_until_complete() |
| 153 | +<asyncio.loop.run_until_complete>`. |
| 154 | +Trio, however, requires one long-running main loop. trio-asyncio bridges |
| 155 | +this gap by providing two event loop implementations. |
| 156 | + |
| 157 | +* The preferred option is to use an "async loop": inside a |
| 158 | + Trio-flavored async function, write ``async with |
| 159 | + trio_asyncio.open_loop() as loop:``. Within the ``async with`` |
| 160 | + block (and anything it calls, and any tasks it starts, and so on), |
| 161 | + :func:`asyncio.get_event_loop` and :func:`asyncio.get_running_loop` |
| 162 | + will return *loop*. You can't manually start and stop an async |
| 163 | + loop. Instead, it starts when you enter the ``async with`` block and |
| 164 | + stops when you exit the block. |
| 165 | + |
| 166 | +* The other option is a "sync loop". |
| 167 | + If you've imported trio-asyncio but aren't in Trio context, and you haven't |
| 168 | + installed a custom event loop policy, calling :func:`asyncio.new_event_loop` |
| 169 | + (including the implicit call made by the first :func:`asyncio.get_event_loop` |
| 170 | + in the main thread) will give you an event loop that transparently runs |
| 171 | + in a separate thread in order to support multiple |
| 172 | + calls to :meth:`~asyncio.loop.run_until_complete`, |
| 173 | + :meth:`~asyncio.loop.run_forever`, and :meth:`~asyncio.loop.stop`. |
| 174 | + Sync loops are intended to allow trio-asyncio to run the existing |
| 175 | + test suites of large asyncio libraries, which often call |
| 176 | + :meth:`~asyncio.loop.run_until_complete` on the same loop multiple times. |
| 177 | + Using them for other purposes is deprecated. |
0 commit comments