Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Guide to Concurrency in Python with Asyncio (integralist.co.uk)
195 points by LiamPa on May 24, 2020 | hide | past | favorite | 76 comments


Forgive me, but this is such a ball of mud. All of the "easy" introductions into these primitives are basically whitepaper length, with long digressions into the high-level vs the low-level and historical vs modern patterns. And nothing gets into error cases or problems with cooperative scheduling or debugging/troubleshooting or where the GIL applies. In 5 years debugging the rats nest of concurrent python code that people are writing right now will make clear that go really got this right. So sad that python did not.

Edit: elsewhere on HN right now:

https://nullprogram.com/blog/2020/05/24/


Agree, asyncio is a mess; Trio gets it right.

https://trio.readthedocs.io/

It's a much simpler model that's just as powerful, and makes it easy to get things right. It eliminates the concepts of futures, promises, and awaitables. It has only one way to wait for a task: await it.

For a theoretical explanation of it's "structured concurrency" model see the now-famous essay https://vorpus.org/blog/notes-on-structured-concurrency-or-g...


Nathaniel has done a great job of drawing parallels between go and goto.

I've been stung, time and again by asyncio.create_task() swallowing exceptions[1], and functions not telling callers about background tasks[2].

For others looking to NOT rewrite all their code using a 3rd party event loop, here's a simple reproduction of nursery's behaviour in raw asyncio -

Hopefully, this will help lift the curse of asyncio :)

[EDIT] A library is also available - https://github.com/Tygs/ayo (with amazing operator overload)

  async def main():
      async with Nursery() as ny:
          ny.start_soon(a_1(), a_2(), ..., a_n())
          ny.start_soon(b_1(), b_2(), ..., b_n())
          .
          .
          .
          ny.start_soon(x_1(), x_2(), ..., x_n())


  class Nursery:
      def __init__(self):
          self.tasks = set()
  
      def start_soon(self, *coros: typing.Coroutine):
          for coro in coros:
              self.tasks.add(asyncio.create_task(coro))
  
      async def __aenter__(self):
          return self
  
      async def __aexit__(self, *args):
          try:
              while self.tasks:
                  tasks = self.tasks
                  self.tasks = set()
  
                  done, pending = await asyncio.wait(
                      tasks, return_when=asyncio.FIRST_COMPLETED
                  )
  
                  try:
                      for task in done:
                          await task
                  finally:
                      self.tasks |= pending
          finally:
              for task in self.tasks:
                  task.cancel()
[1] https://stackoverflow.com/questions/60287285/starlette-async...

[2] https://github.com/encode/starlette/issues/947


trio_asyncio lets you write trio code that uses asyncio libraries.


Thanks for the pointer, hadn't heard of trio. Will check it out.


It is absolutely a ball of mud; asyncio is an embarrassment and what these "easy" introductions never get around to telling you is that it's essentially impossible to write correct asyncio code.

Luckily, Trio now exists. Trio has a consistent and simple story for cleanly handling errors and cancellations. Debugging is still a little difficult, but at least code written with Trio has radically fewer problems to debug.

Here's a link on the Trio design: https://trio.readthedocs.io/en/stable/design.html

There are some more great articles and talks, but I don't have any links at the ready, sorry.


> go really got this right

It's also exciting to see that Java has taken the same view, and is well on its way towards delivering it (https://wiki.openjdk.java.net/display/loom), or even improving on it (https://wiki.openjdk.java.net/display/loom/Structured+Concur..., see also: https://vorpus.org/blog/notes-on-structured-concurrency-or-g...)


I'm a long time fan of Python (and still am), but I completely agree that Go got concurrency right.


Why would the Gil apply to asyncio, which is strictly single threaded?


The post also discusses concurrent.futures.


Can't you just use the ProcessPoolExecutor[1] for CPU heavy stuff? AFAIK, GIL isn't much of an issue with I/O tasks.

[1] https://docs.python.org/3.8/library/concurrent.futures.html#...


You are right, GIL isn't an issue with I/O-bound tasks.

The article didn't limit its focus to I/O, though. It felt more like an explanation of modern concurrency and asynchronous options in Python (besides just threading and and multiprocessing).

The only practical issue with the GIL in Python is that it forces you to use new (heavy) Python processes to parallelize computationally heavy tasks. Not so much of a concern depending on your application, but it is a gotcha for people new to Python.

The real issue, though, as your parent poster mentioned, is the number of different ways to access asynchronicity and concurrency in modern Pythons. It really does run counter to Python PEP20 [0] and can lead to some communication difficulties among developers.

[0] https://www.python.org/dev/peps/pep-0020/


Just an aside from the argument here, I think almost no python API actually follows the zen of python. It's certainly a nice idea, just incredibly hard to actually live upto in the real world.


Which is also single (OS) threaded.


Why do you say that?

I believe concurrent.futures is meant as a way for asyncio to spawn threads or processes -- which lets you essentially call sync code from async and not have it block the event loop.

From the docs[1] -

The concurrent.futures module provides a high-level interface for asynchronously executing callables.

The asynchronous execution can be performed with threads, using ThreadPoolExecutor, or separate processes, using ProcessPoolExecutor. Both implement the same interface, which is defined by the abstract Executor class.

[1] https://docs.python.org/3.8/library/concurrent.futures.html


Python's threadpoolexecutor is still single OS-threaded. There will only ever be one thread executing in parallel (due to the GIL). Basically, in python, asyncio and threadpoolexecutor are almost entirely mappable to each other. Neither can accomplish more than the other.

Multi-process code can, of course, address some of these issues, but it comes at the cost of spinning up multiple OS processes and costly inter-process communication etc.


This doesn’t sound quite right. Unless they changed something threadpoolexecutor does involve multiple OS threads, and is not single threaded. Sure the GIL puts a big limitation on parallel execution, but it is most definitely different.

For one, regardless of the GIL it is still possible to have races in multithreaded python code. You have to worry about RMW because the threads are arbitrarily preemptible at a bytecode boundary. This is not the case with asyncio.

Additionally you can release the GIL when making calls to C code. Asyncio is giving you concurrency by using nonblocking calls, you can’t run two CPU bound threads in parallel with it. But you can do this with python threads. So it’s not true that neither can accomplish more than the other.


The only issue I see re: the GIL is that ThreadPoolExecutor (by its name) can mislead them into thinking that they are using proper threads when in fact they are still constrained by the GIL.

It's a confusion that has been quickly resolved every time a team mate of mine has run into the issue, but I have seen it happen multiple times (at different places).


The first design constraint with so much python development is the battle to stay single threaded. As you point out, debugging concurrent python code is horrendous.


I think this could be done well, single-threaded, but Python reinvented the wheel a half-dozen times now, and I don't think any of the wheels are that great. Having them all in the system at the same time, and without great connections, is what makes this a ball of mud.

There's very little glue between them. And none of it is duck typed, like the rest of Python. If I switch between them, I need to rethread the entire system.


To be fair, debugging all concurrent code is horrendous. If you can use multiple processes instead of multiple threads, you should.


Here is the thing though, and I am gonna phrase this very carefully as I have stepped on some toes before and got downvoted.

Imagine a naive young scientist who has been "trained" to code in Matlab. He is not a computer scientist, of course, but he profits if his program goes fast, even before actual computer scientists get involved to make the thing proper. Indeed that may make the difference between the project getting off the ground or not going forward. Such cases have been known to exist.

Now, he comes across some code that can not be written in pre-optimized matrix routines, but nevertheless populates deterministic addresses in a container without any further side effects. It could wait for some IO, or perhaps it just does a thing that takes a while on the CPU core (like iterations of something with results getting saved somehwere). In any case, it's gotta go into a for loop and that's slow for most languages of that sort.

Given that he has a good number of cores, threads etc. available, he figures it'd be pretty cool if the thing run in parallel.

So he changes the "for" statement into "parfor". Matlab then starts a CPU pool and runs the loop to completion in parallel on both threads and cores without further issues. The whole thing just runs x times faster.

My point here is this: The whole thing in Python is complicated. It is so, because other languages, like Matlab, make it laughably easy. The Pythonic way would have been to offer such an easy option. However, if you are not well-versed in serious coding, then concurrent anything in Python is hella complicated. If you disagree, you likely know more about multi-x than the average person typing things into a computer.

I do not doubt that such a simple approach has its limitations. But it seems to me that in practice - and in particular when I look at most of the examples presented - such a simple way would have been completely sufficient. I do readily accept that I may be completely wrong.

edit: And now I realize that your verb was "debug" and not "write". Sorry.


Yeah, I support these people regularly. If you can replace "for" with "parfor" and it just goes faster, well, God bless you.

In my experience, 90% of these novices haven't the faintest clue of what's going on.

"It worked before, but now I'm running out of memory."

"Why are you telling me that I'm crushing disk I/O?"

"I went from 1 core to 40, and now it runs slower!"

"I requested 32 nodes--why doesn't it run 32x faster?"

"I requested one core, but MATLAB saw the awesome stuff in /proc/cpuinfo and ran 40 cores. Why is it even slower than ever?"

Bless the clueless, for they shall inherit the earth. (But please, don't let them write coronavirus models in public...)


Absolutely the worst. Ten years ago I encountered a bug in a wireless networking stack that would crash the whole network of nodes. It was in the worst possible location that was highly dependent on timed sending where every nanosecond mattered. This meant i could only use leds to indicate what was going on, as a print or anything the like would screw the timers enough to break the network. Can’t actually remember what caused it in the end.

It took me 6 weeks to debug and fix. I did nothing but debugging at the most primitive level. Probably could have done it more efficiently, but I was young and inexperienced.

Worst and most boring 6 weeks of my life caused by concurrent debugging.


About 10 years ago there was a bug in driver to the Intel wireless card, it crashed when it received 802.11n packet. That bug I remeber well, because it was responsible for me stunning Ubuntu. They knew about that bug (it was reported when they were getting ready to release) and still went forward with the release. I'm wondering if that was related.


s/stunning/shunning/ ?


I think I meant to write "dropping" it's the swyping keyboard. I really hate "typing" over the screen.


Must have been really bad if one wireless remote node can crash all the other nodes.


Coupling can be a bitch. Worked in a shop once that had GPFS (shared filesystem) everywhere. Their rule was "no swap space on any node ever". Weird. Why not? Because apparently slow thrashing on one node will crush GPFS performance on all nodes, without ejecting the failing node. Ugh.


This looks great. I wish python would have made the event loop generally more transparent to the end user. Why didn't they just use a single global loop? Sorta like javascript and golang. It would have been more pythonic too. Anyone doing heavier context switching could've had their own loop management.

Making the event loop self-managed added a ton of clunkiness in aio apis (use-your-own-loop, loop lifetime management, etc) and becomes mentally complex for newcomers. Theres also the issue that these huge aio frameworks rewriting the same TCP clients have emerged only differentiated by their loop management patterns.


The more time I spend with Python the more reasons I find to hate it.

However, it's still my main language. Everything I end up making just ends up being written in Python because that's the language I know the best.

I could learn another language, and I have learned other languages, but Python seems to be the language that mentally clicks with me the most. I think it's because it's the first language I learned.

Suffice to say, there are so many things wrong with Python and they're all compromises that were made to appeal to wider audience. Unfortunately, I don't think that the "wider audience" is those who are looking for concurrency.


We agree. I think python is great. I actually asked because python usually makes things dead simple and straightforward.

> Unfortunately, I don't think that the "wider audience" is those who are looking for concurrency.

I don't buy this. Many network heavy shops use Python so concurrency is relevant. The language is used in web, finance, cloud, data pipelining/etl, and so many more. Dropbox, Robinhood, Spotify, Instagram, Uber, Google, etc, are the "wider audience" and they certainly care about concurrency.


Me too. As I know more and more about languages and computers, the more I hate it but I end up doing 85% of my projects because with it I can focus on ideas, not implementation details.


>Why didn't they just use a single global loop? Sorta like javascript and golang.

I imagine the problem is, that neither JS or Golang have to deal with, is Python supports plain old threads. If you have a single global event loop, and then you try execute a future what should happen?

1. Should every every thread have its own loop? Then how would you send futures to another thread.

2. Should the global loop only exist on a single thread? Then you would have to pay a synchronization tax every time you pushed a task to the loop.

Not saying the Python solution is 100% right (I've only thought about it for 5 minutes), but I can see how being compatible with threads has caused this situation. I believe the default now is you don't have to think about loops, and Python does the right thing for 99% of use cases.


If you need to do CPU bound computations use a thread pool.


Why this being downvoted:

You need to use a process pool in Python, thread pools are for I/O bound tasks and cannot use more than 1 core.


I didn't downvote him, but his response isn't at all relevant to what was being discussed. I was discussing the API design of asyncio and how they had to add the "loop" parameter to ensure the reactor would be thread safe by default.


Since version 3.7 you don't need to touch the loop for nothing, also some old signatures had deprecated the loop param... It's almost the same as if you work with node, but with a bit more sugar (tasks, gather, wait, wait_for) When you work with asyncio daily it's so natural and fun.


Agreed, Python’s async model is the easiest one for me to think about by far and there’s so many high quality libraries


Have you used C#'s or Akka's or Go's?

I find 2 of those to be easier to reason about and all to be superior.


yes, goroutines are fast and easy... but this thread is about python. I love both languages, and go is fast and safe, but, I also like python (it's fast developing... )


> yes, goroutines are fast and easy... but this thread is about python.

The OP was commenting on what in his opinion was the easiest. That explicitly represents a judgement of Python's features compared with those offered by other languages.

Are we supposed to only praise Python in threads about Python?


Thought, you misunderstood my opinion, mostly that sometimes there are project constraints, usual the language.. between the python constrain asyncio it's not bad.


Sorry, but after using Golang with its very simple and powerful goroutines and channels approach to concurrency, this seems like a convoluted mess. When I need concurrency, I certainly don't think of Python as the right tool.

Python seems to have lost its way after the 2 to 3 shift and is no longer what I'd consider "pythonic".


I think programming language design as a having a large random component. New languages pop up every day, and it's really hard to predict which constructs will resonate well with human brains.

Guido was competent, but not nearly as brilliant as, for example, Larry, but Python 2 was dramatically better than Perl. I think this is mostly by chance. Hundreds of new languages come out every year, and by whatever fluke, a few of them really work. Python 2 was that.

In an ecosystem like that, past performance is no indication of future performance. There are very few language designers who did well more than once (Guy Steele is the only one I can think of who wasn't a one-hit-wonder).

So I too felt like Python 3 was more of a step backwards than forwards linguistically. But at this point, it has features I need which Python 2 lacks, so I'm mostly working in Python 3. But it definitely feels (unnecessarily) less clean.


Python's origin story as being a replacement to Perl is so completely out of scope compared to what modern scripting languages are doing it's laughable.


If you want to integrate with the data science ecosystem, would you use modern Python for APIs as well, or do Go services with minimal Python bits?

EDIT: coming from someone who has had great experiences with FastAPI and Uvicorn combined with Dask and Redis.


One thing I haven't seen any blogs write about is that multiprocessing in 3.8.x uses `spawn()` and not `fork()`[0] on MacOS. Granted, most applications are not running on OS X Server, but an update that changes a low level API like that led to some issues where running code locally will fail when running it remotely will work. The following code will run anywhere except on MacOS on 3.8.x, where it crashes with `_pickle.PicklingError` as it tries to resolve the name `func` that is not in the new stack:

    import multiprocessing
    some_data = {1: "one", 2: "two"}
    func = lambda: some_data.get(2)
    process = multiprocessing.Process(target=func)
    process.start()
[0]: https://bugs.python.org/issue33725


I'm in the process of adding 3.8 support to Airflow. This was the first (but not the last) obstacle in doing so.


To those trashing Python in favor of Golang: A compiled language with no OO is not a replacement for Python. Let's talk about the complexity of putting code generators in all of your projects. I've seen real golang projects and the complexity gets moved into your repo.


> To those trashing Python in favor of Golang: A compiled language with no OO is not a replacement for Python.

Why?

I mean, come on. Who in their right mind would ever claim that Go is not a good choice to develop web services?

And are we supposed to turn a blind eye to Python's problems such as sub-par performance and the GIL?

I understand how Python fanboys will pick Python over alternatives whether that makes technical sense or not, but asserting that any compiled language cannot be used to deliver software that has been implemented with Python, and that explicit support for OO is a relevant requirement, is something that flies in the face of reason.


With some very simple sharding (we are talking splitting lists and dictionaries) you can bypass the GIL and not balloon your memory usage. To do so you would use aiomultiprocess which inherits from asyncio classes (and therefore interfaces) but lets you await on methods running in their own processes.

https://github.com/omnilib/aiomultiprocess


though i'd say that with Go's approach to generics (wrapping everything in `interface{}`), people used to dynamically-typed languages should feel right at home ;)


Shameless plug for what is essentially my own much shorter intro to asyncio: https://charemza.name/blog/posts/python/asyncio/I-like-pytho...


Or just use curio or trio and have an infinitely better time with async in python :)


I see no reason to use curio or anything instead of standard asyncio. Works really well for me. Queue tasks, they finish at their leisure, create futures and locks to control flow, all really easy and bugless code. And you get async redis, async http, a lot of asyncio functions for free. So no, I will not change it for some "really well architected library that takes away all your pains" because there aren't any.


The reason is that curio is well-designed and has a reasonably compact story for how things work. It fits in my head. And the "cool new things I can do with it" to "how hard is it to learn" ratio is pretty high.

Asyncio on the other hand, feels like being beaten with a stick. It's so complicated that normal humans are never really going to understand it all.

On top of that, it's been morphing all through the 3.0 series, so there isn't even one spec to learn--it's more like a set.


What do you mean compact story? How do humans not understand asyncio? I think I pretty much do, and I use it in production, with several interconnected services. How has curio been morphing? How is morphing actually a good thing? Isn't it bad that an api is unstable?

Overall I understood little to nothing from your comment.


The curio module has not been morphing, which is part of what I like about it.

The async module has been changing, which like you, I dislike. The "right" way to do asyncio would have been to properly mull the design into its final form and then introduce it into Python3. Instead, it appears to have been dribbled in piecemeal over several releases.

Beyond that, it's just plain complex and confusing. I haven't bothered to use it for anything so far. I'm hoping someone will show up with a machete and cut it down into something usable. Right now, it resembles C++.


Yeah I haven't actually used it until 3.6.9 where it turned into its current final form. What is confusing about asyncio and how does curio solve that problem?


"I can use this wrench to drive a nail, what would I want a hammer for?" is basically how this reads.

I'm not saying software can't be written with asyncio, I'm just saying curio and trio are better.


What do you use for HTTP message handling?


Well, there is no request router in asyncio-http, so I have to use async views in django or if I need something lightweight I go with Quart. But if I was given the proper time, I'd probably invent my own routing on top of asyncio-http using google's re2 to parse the REST requests like I did here some years ago. https://github.com/nurettin/pwned/blob/master/server/server....


By asyncio http do you mean aiohttp?

Besides the standard way, you can also register routes similarly to DJango and Flask: https://docs.aiohttp.org/en/stable/web_quickstart.html#alter...


oh I haven't noticed it had a router, thanks


curio is by David Beazley, [1] which should be reason enough to investigate it.

[1] once memorably dubbed "the people's champion" in the introduction to one of his PyCon speeches.


There's this really interesting blog post Im not remembering about nodejs and async concurrency style as compared to golangs one. I cannot remember the author's name, but it goes into length as of comparing async functions and the differences between normal regular func and the async ones, and calling async func only on async scope and so on. Only I remembered I found it retweeted by Brad Fitz. I though maybe someone could guide me to it here


Possibly "What Color is Your Function"? One of my favorites on concurrency approaches. https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...


I've found this [1] screencast on the low-level principles of async in Python an amazing resource to understand async at a deep level.

[1] https://www.youtube.com/watch?v=Y4Gt3Xjd7G8&list=PLKLmIMXskJ...


I don't see anything about I/O in this guide.

Can anyone recommend a guide which explains how to actually do I/O using asyncio: how to accept connections, how to write to and read from a socket?


Every time I see concurrency and python together, I’m immediately turned off by it. First of all, it probably won’t be a true “concurrency” due to GIL and by the point you are looking for “advanced” libraries that allow you to do whatever you are trying to do, maybe it’s time to switch languages.


Did you mean: not true parallelism?


Asyncio detractors have been drowned in a stream of assurances that, really, it's not that bad.

Obviously it is, or this wouldn't be post #800 that promises to finally make asyncio clear to newcomers.

Twisted sucks. It was a joke. Now it's been tacitly blessed to the point you can sprinkle the `async` keyword all over the place. Good luck. I hope you don't forget about any spots.

I truly think part of the reason Guido left is because he couldn't defend this shit.


Yes Twisted had its draw backs but that was well over 15 years ago. Async io is literally like a couple keywords and a library with a few classes you will use for everything. No more callback/errback chaining or function decorators every single method for inline callbacks.

No one in these comments are offering any palatable alternatives. A compiled language with no OO is not a replacement for Python's Asyncio.


Not to stoke the flame, but I never used Twisted, where did it fail?


Bad docs.

Everything else about it is solid as hell.

Pathlib comes from Twisted IIRC.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: