-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add async/await syntax to FULL_BUILD #2985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This adds the `async def` and `await` verbs to valid CircuitPython syntax using the Micropython implementation.
Consider:
```
>>> class Awaitable:
... def __iter__(self):
... for i in range(3):
... print('awaiting', i)
... yield
... return 42
...
>>> async def wait_for_it():
... a = Awaitable()
... result = await a
... return result
...
>>> task = wait_for_it()
>>> next(task)
awaiting 0
>>> next(task)
awaiting 1
>>> next(task)
awaiting 2
>>> next(task)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: 42
>>>
```
and more excitingly:
```
>>> async def it_awaits_a_subtask():
... value = await wait_for_it()
... print('twice as good', value * 2)
...
>>> task = it_awaits_a_subtask()
>>> next(task)
awaiting 0
>>> next(task)
awaiting 1
>>> next(task)
awaiting 2
>>> next(task)
twice as good 84
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration:
```
Note that this is just syntax plumbing, not an all-encompassing implementation of an asynchronous task scheduler or asynchronous hardware apis.
uasyncio might be a good module to bring in, or something else - but the standard Python syntax does not _strictly require_ deeper hardware
support.
Micropython implements the await verb via the __iter__ function rather than __await__. It's okay.
The syntax being present will enable users to write clean and expressive multi-step state machines that are written serially and interleaved
according to the rules provided by those users.
Given that this does not include an all-encompassing C scheduler, this is expected to be an advanced functionality until the community settles
on the future of deep hardware support for async/await in CircuitPython. Users will implement yield-based schedulers and tasks wrapping
synchronous hardware APIs with polling to avoid blocking, while their application business logic gets simple `await` statements.
|
While we wait for the list of microcontrollers that are too micro to async, here's an example that shows how exception propagation is able to be handled in an async/await context. |
This lets you register async tasks that are driven until completion, and supports a non-blocking sleep() within those tasks. Depends on async/await syntax introduced at adafruit/circuitpython#2985
|
And here's an async lib I threw together for this: https://github.com/WarriorOfWire/circuitpython-utilities/tree/master/budget_async You can use it kind of like an asyncio event loop. This was really easy to put together; there is much opportunity for others to make better things. It ties async/await to an event loop that you'd most likely call next() on in a tight while: True loop in your main application function. While in a coroutine you can |
|
Do any of your examples work in both the modified CircuitPython and in Python 3? I was not able to modify your examples to work in both interpreters. |
|
No; Micropython implemented the This impacts developers of "nuts and bolts" features, like that budget async lib, but it should not impact regular use. I.e., if you have a suitable concurrency lib in your project, you just use it and can choose to write methods with I'm aiming to help us get mileage on the syntax, not specifically to Solve The Problem universally. I'd like to switch up the implementation a little to be closer to CPython's but allowing the syntax and trying it out is the first step. |
|
@WarriorOfWire Thanks for making a PR for this! It's nice to see a PR proposal. I don't want to introduce an async API that is incompatible with CPython because then code will be built on top that we'd have to break later. CPython compatibility also means libraries can be developed and tested in CPython too. I know MicroPython has been doing a lot of asyncio work recently. Do you know if they've switched away from generators and are now CPython compatible? It's been a while since we merged so upstream may have already evolved. If that's the case, we should consider merging in upstream (a large task but this would be another reason to.) Thanks! |
It's still backed by yield_from which uses iter and yield.
Do consider that end user code is written the same way; the coroutine vs. generator thing only concerns library developers. By having the syntax generally available we can gather feedback on concurrency styles that users like, and decouple implementation of |
Thanks for the links!
I understand that but in this case library developers are our users.
How big is the
Thanks! I'd like to have the async and await keywords but I want them to work like CPython. @dpgeorge @jimmo Any insight why MicroPython's asyncio isn't CPython compatible? |
|
Asyncio is a pretty complex topic and there's been a lot of work done on it in MicroPython over the years. Support for coroutines via generators was done for efficiency and for the most part users will not notice any difference here. Our |
|
@dpgeorge Thanks for the reply! What sort of efficiency? Execution speed or implementation. I'd much prefer a CPython compatible solution. |
|
Mainly efficiency of implementation. There are lots of layers involved with an asyncio implementation and we've prioritised making the user-facing layers CPython compatible. I don't think it's realistic (or necessary) to make everything CPython compatible, for example it probably requires reference counting to get task clean-up to work the same. So I guess the question is: what does "CPython compatible" mean to you? |
|
In making my weak scheduler PoC I saw that the lack of |
|
To be clear though, it is really just that you can't implement an |
@WarriorOfWire are you able to provide/link to some code which uses |
|
Awesome! |
|
@dpgeorge while you're in the neighborhood - how would I implement a suspending function in Python using async/await that works both in CPython and Micropython? Consider: This works in CircuitPython but not in CPython. It is an abuse of the encapsulation, but I'm not sure what alternative means to trigger a generator suspension would be more appropriate given that I'm limited to using Python for this. |
|
Right, figured out a way that works in both to provide a "sleep": It's a little circuitous but it will do until we have a C module in place. |
|
@WarriorOfWire you can do |
|
So this bad Python scheduler serves as a proof that there is a subset of functionality that overlaps sufficiently with CPython to write a meaningful pure Python coroutine scheduler (which should not be the ultimate goal here imho). The async/await syntax of my demo application has been unchanged through this tumult BudgetScheduler has remained API compatible even through misunderstandings. Creating syntax-compatible (CPY-CircuitPy) Python libraries wrapping C device drivers is eminently plausible. Notwithstanding all of this, it would still make sense to me if @tannewt does not want to take syntax that does not have full vertical C support. I will continue to benefit greatly from my bad scheduler, as it handily beats ad-hoc state machines, but in truth it is not a suitable general purpose substitute for |
Yes you're right, this suffices for compatibility: |
@dpgeorge Thanks for jumping in on this! I really appreciate your input. My philosophy is that "CPython compatible" means that any code written in CircuitPython that only imports modules available in CPython works without modification in CPython. The corollary of this is that any CircuitPython-only code must import a module not available in CPython. This means that CircuitPython-only code will error consistently early at import time. (It also gives us a hook to provide a python library in CPython to implement the functionality.) I don't know a lot about asyncio so please correct me if I'm wrong. It seems to me that by having |
|
You can use generators like coroutines even back in Python 2, ever since yield was introduced — in fact the early async proofs of concepts were done that way. |
|
I think that at some point they actually added a check to async/await that raises an error if you call it with a generator, but that was relatively late in the development. |
|
Anyone want to do the work to make sure we can't use the returned object differently than we would in CPython? Otherwise I'll close this PR. |
|
Closing for now. Anyone should feel free to pick this branch up and make a new PR. My concern as-is is that the returned object should have a Python API that is a subset of the object returned in CPython. I don't care how it is implemented internally. |

This adds the
async defandawaitverbs to valid CircuitPython syntax using the Micropython implementation.Consider:
and more excitingly:
Note that this is just syntax plumbing, not an all-encompassing implementation of an asynchronous task scheduler or asynchronous hardware apis.
uasyncio might be a good module to bring in, or something else - but the standard Python syntax does not strictly require deeper hardware
support.
Micropython implements the await verb via the iter function rather than await. It's okay.
The syntax being present will enable users to write clean and expressive multi-step state machines that are written serially and interleaved
according to the rules provided by those users.
Given that this does not include an all-encompassing C scheduler, this is expected to be an advanced functionality until the community settles
on the future of deep hardware support for async/await in CircuitPython. Users will implement yield-based schedulers and tasks wrapping
synchronous hardware APIs with polling to avoid blocking, while their application business logic gets simple
awaitstatements.