Are goroutines roughly equivalent to python's asyncio
tasks, with an additional feature that any CPU-bound task is routed to a ThreadPoolExecutor
instead of being added to the event loop (of course, with the assumption that we use a python interpreter without GIL)?
Is there any substantial difference between the two approaches that I'm missing? Of course, apart from the efficiencies and code clarity that result from the concurrency being an integral part of Go.
I think I know part of the answer. I tried to summarize my understanding of the differences, in order of importance, between asyncio
tasks and goroutines:
1) Unlike under asyncio
, one rarely needs to worry that their goroutine will block for too long. OTOH, memory sharing across goroutines is akin to memory sharing across threads rather than asyncio
tasks since goroutine execution order guarantees are much weaker (even if the hardware has only a single core).
asyncio
will only switch context on explicit await
, yield
and certain event loop methods, while Go runtime may switch on far more subtle triggers (such as certain function calls). So asyncio
is perfectly cooperative, while goroutines are only mostly cooperative (and the roadmap suggests they will become even less cooperative over time).
A really tight loop (such as with numeric computation) could still block Go runtime (well, the thread it's running on). If it happens, it's going to have less of an impact than in python - unless it occurs in mutliple threads.
2) Goroutines are have off-the-shelf support for parallel computation, which would require a more sophisticated approach under asyncio
.
Go runtime can run threads in parallel (if multiple cores are available), and so it's somewhat similar to running multiple asyncio
event loops in a thread pool under a GIL-less python runtime, with a language-aware load balancer in front.
3) Go runtime will automatically handle blocking syscalls in a separate thread; this needs to be done explicitly under asyncio
(e.g., using run_in_executor
).
That said, in terms of memory cost, goroutines are very much like asyncio
tasks rather than threads.
I suppose you could think of it working that way underneath, sure. It's not really accurate, but, close enough.
But there is a big difference: in Go you can write straight line code, and all the I/O blocking is handled for you automatically. You can call Read, then Write, then Read, in simple straight line code. With Python asyncio, as I understand it, you need to queue up a function to handle the reads, rather than just calling Read.