I'm of two minds on this point. Uncolored async is simpler language design and works well for _most_ cases but provides way fewer guarantees than colored async, with the potential to break badly in some cases.
For instance, there are many system-level data structures that are not allowed to migrate from one thread to another (e.g. Linux mutexes or Rust's Rc) or sometimes from one core to another. If you adopt uncolored async (unless perhaps you're using a thread-per-core scheduler), you just can't manipulate these data structures. At all.
Which means that you can't be a system programming language (for some definition of system programming).
You don't want to use blocking mutexes anyway with async.
or Rust's Rc
This is only half true. The danger is that two `Rc` that point to the same data are in different threads. But it should be safe to move all of them at once from one thread to another, which is exactly the case if all the `Rc`s involved live inside a `Future`. The problem is that this is a non-local property that's hard to encode in the type system.
FYI that's known to be unsound due to thread locals. And more generally it doesn't seem to give much attention to safety (see for example how it allowed unsound scoped tasks, or the fact it allows doing unsafe operations in some of its macros due to wrong scoping of `unsafe` blocks).
Go's approach to async programming is indeed simpler, but its FFI and handling of stateful coroutines can introduce complexity and overhead when bridging with external code. Using CGO is slow because it requires synchronization of the coroutine state between libc and Go. Everyone avoids CGO whenever possible, so it is not a solution.
Rust chose a different path that is not more complex, but the complexity lies elsewhere.
Just an extra bit of clarity: the relative slowness of CGo calls is a conscious trade-off that enables Go threads to be much lighter weight. It's not an inherent aspect of the design, but a trade-off you pay for wanting more lightweight green threads. Choosing to not follow the age old C ABI internally means you have to bridge that gap if and when you come to it.
Comments
I believe Golang uses a similar model but much more simple to use. Uncolored async is the way to go
I'm of two minds on this point. Uncolored async is simpler language design and works well for _most_ cases but provides way fewer guarantees than colored async, with the potential to break badly in some cases.
For instance, there are many system-level data structures that are not allowed to migrate from one thread to another (e.g. Linux mutexes or Rust's Rc) or sometimes from one core to another. If you adopt uncolored async (unless perhaps you're using a thread-per-core scheduler), you just can't manipulate these data structures. At all.
Which means that you can't be a system programming language (for some definition of system programming).
By the way, if you wish to test uncolored async in Rust, you can find an implementation here: https://github.com/Xudong-Huang/may .
You don't want to use blocking mutexes anyway with async.
This is only half true. The danger is that two `Rc` that point to the same data are in different threads. But it should be safe to move all of them at once from one thread to another, which is exactly the case if all the `Rc`s involved live inside a `Future`. The problem is that this is a non-local property that's hard to encode in the type system.
FYI that's known to be unsound due to thread locals. And more generally it doesn't seem to give much attention to safety (see for example how it allowed unsound scoped tasks, or the fact it allows doing unsafe operations in some of its macros due to wrong scoping of `unsafe` blocks).
A simple call to https://pkg.go.dev/runtime#LockOSThread solves that.
Pay the cost of backwards compat when you need it, don't pay it when you don't need it.
Go's approach to async programming is indeed simpler, but its FFI and handling of stateful coroutines can introduce complexity and overhead when bridging with external code. Using CGO is slow because it requires synchronization of the coroutine state between libc and Go. Everyone avoids CGO whenever possible, so it is not a solution.
Rust chose a different path that is not more complex, but the complexity lies elsewhere.
Just an extra bit of clarity: the relative slowness of CGo calls is a conscious trade-off that enables Go threads to be much lighter weight. It's not an inherent aspect of the design, but a trade-off you pay for wanting more lightweight green threads. Choosing to not follow the age old C ABI internally means you have to bridge that gap if and when you come to it.