So for me, there really are only 2 concurrency choices: coroutines or multiprocessing. And generally, if I find myself reaching for multiprocessing I seriously evaluate if the logic shouldn't be ported to a different language.
So for me, there really are only 2 concurrency choices: coroutines or multiprocessing. And generally, if I find myself reaching for multiprocessing I seriously evaluate if the logic shouldn't be ported to a different language.
I've seen more than once people rewriting thread-based code to coroutines, despite not having any noticeable benefits other than being more sexy. In fact I was one of those people almost a decade ago, rewriting the whole networking portion of the app only to achieve ~5% performance improvement. One should always measure the overhead instead assuming how big it is.
David Beazley does a much better illustrating the nuances than I ever could:
https://www.youtube.com/watch?v=Obt-vMVdM8s
Including a wild phenomenon where a threaded Python program's performance can degrade by increasing number of CPU cores available to a system.
Coroutines avoid this by not requiring individual thread scheduling by the OS. So they effectively release the GIL like threads on IO but avoid OS level thread switching.
Another interesting example he talks about is IO-bound threads competing with CPU-bound threads, where CPU-threads are getting seemingly unfair advantage. But the same example would only get worse with coroutine implementation: since there's only one thread, CPU bound code would just block IO forever. IO-bound code would not just be slow, it would never execute.
What David illustrated is that GIL makes it harder to mix CPU- and IO-bound tasks in the same process. But coroutines are not the solution here. Solution is not to mix them.
> What David illustrated is that GIL makes it harder to mix CPU- and IO-bound tasks in the same process. But coroutines are not the solution here. Solution is not to mix them.
Which I believe is my point and why I forwarded the personal notion that there really is only two acceptable concurrency models in Python. Coroutines solve cooperative scheduling for things that can be cooperatively scheduled. Threads _could_, but in Python specifically threads are strictly inferior as they have additional overhead by nature. In slightly different phrasing, Threads can be nearly or even acceptably equivalent to coroutines in performance, but they will never be better so long as Python has a GIL.
For everything else there is Multiprocessing (for example segmenting CPU processing from IO over separate processes).
In terms of performance, sure. But performance is rarely your only priority. A lot of the times you already have existing code that can be switched to threads with 2 lines of code. Instead, folks choose to rewrite the whole app in asyncio/nodejs because threads aren't sexy. Hey, I've done it.
But today I'd rather have inferior working app in one day, than a completely rewritten app in a few weeks, with a mental model that half engineers don't understand and don't have experience with. Maybe in a few years you will be forced to rewrite it in asyncio. And that would be the right time to do it: when it's a business demand, not when few perfectionists are annoyed.
Having used both threading and various coroutine libraries (gevent prior to asyncio and asyncio more recently), I find coroutines easier to work with in modern versions of Python. So I don't find them superior strictly on performance; I also consider them more productive due to ease of use.
But I wouldn't disagree with someone choosing to use threads because they find them easier to grok than asyncio.
Time spend rewriting networking
Code because more complicated: cooperation was moved to code instead of being handled by OS (to be fair this was way before asyncio, it was gevent).
Code became alien to the team. I was the only one who knew how it worked.
In the end, the only problem I solved was the problem we didn't have: "not using coroutines". Sure, threads suck at scale. But you should build things for scale when it's necessary, not when it's fun.
The multi-threaded concurrency style can be a great choice if your performance requirements are low and you only want a small amount of concurrency. Then it can be very easy to emulate a Golang-esq model, by having just a few threads which are communicating by writing data via queues to one another.