I just watched this talk by David Beazley about Python’s newish
await functionality. Beazley is also known for his Curious Course on Coroutines and Concurrency, which is also wonderful. I’m not sure this talk really sells the paradigm to folks who haven’t been bitten by concurrent programming though, and gets a little bogged down in the yield implementation. I’m also surprised he didn’t reference previous async/await implementations like C#. That said, the talk is really engaging and well-done, and has a lot of great info on
I felt like this would be a reasonable time to jot down some thoughts I’ve had about concurrent programming, which I’ve mentioned conversations but haven’t ever written down. I’m certainly not an expert - this is informed by work in C++ using threads, bare-metal C using hardware interrupts and callbacks, Python using gevent, and Julia using the native Coroutine support, but there are probably other major types of concurrency that I’m missing. Let me know!
I find myself dividing concurrency systems into several types:
- preemptive multitasking, e.g. os-level threads
- context-switches can happen at any point in your code, code might even be running in parallel (on multi-core). Accessing shared memory generally needs to be protected with synchronization primitives like mutexes.
- cooperative multitasking, e.g. gevent-style green-threads, or Julia Tasks
- context switches only happen when a task explicitly yields, or does something blocking like I/O. As long as you don’t have any blocking calls in your critical section you should be safe. Unfortunately you have no idea if a function you’re calling ends up calling something blocking (maybe it prints to a log). In gevent you actually monkey-patch the blocking IO functions so that instead of blocking the whole thread at the OS level it just blocks the current task and hands control back to the event loop.
- explicit callbacks
await(found in C#, python)
- To me this seems like a sweet spot where you can write sequential code, but anywhere that might possibly context-switch is marked by
The blog post linked in the video is well-worth reading. One issue I take is that there is an advantage to explicitly dividing functions into synchronous and asynchronous, in that you know when your task might block, and you can make sure it’s not in the middle of a critical section. That said, if you’re not using shared memory to pass information between tasks it’s less of a problem. The reason Go’s coroutine model works so well is because it’s coupled with Channels as the main mechanism for passing information between coroutines, so you generally don’t have to worry about unexpected context switches.