5 ms·
Because native multithreading is expensive when it comes to (a) memory consumption per thread of execution and (b) context switching. Also, if your program spen
by eestrada 4y ago
Because native multithreading is expensive when it comes to (a) memory consumption per thread of execution and (b) context switching. Also, if your program spends a lot of time just waiting on outside resources (most notably slow/blocking IO), coroutine concurrency is bother faster and more lightweight. If your program is CPU bound, then coroutines/async have little-to-no value over native multithreading.
That being said, yeah, most async workflows are obtuse and difficult to follow and they force all consumers of your interface to also use the async primitives if they want the benefits. Go seems to be one of the few languages that does async/coroutines right.
See: https://eli.thegreenplace.net/2018/go-hits-the-concurrency-nail-right-on-the-head/ https://eli.thegreenplace.net/2018/go-hits-the-concurrency-n...
- rcme 4y agoWhat I like about Go is that each co-routine is entirely synchronous, and the only concurrency primitives you're given are channels and mutexes. This makes your code so simple and easy to understand. I just looked up Kotlin's co-routines, and there are a number of annoying hoops you need to jump through: 1. Co-routines can only be built by calling runBlocking or coroutineScope. 2. Functions that are async buy should act like blocking calls in a co-routine need to be marked with `suspend` 3. There are a bunch of primitives you need to use like `launch` or `async`. 4. There are built-in options to make the co-routines lazy, but it makes the API feel sloppy. 5. Rather than locks, Kotlin encourages explicit thread dispatching. Some times you need to do this in Go too, like when using a C library, but in general it's rare.
- ackfoobar 4y ago`launch` is just the `go` keyword. `async` stores the result in a `Deferred` (or `Promise`), which is an abstraction for parallel decomposition. Other than confining UI work to the main thread, I don't think I have seen thread confinement as an alternative to locks.
- rcme 4y agoThat's kind of my point. Go has the `go` keyword. Kotlin has runBlocking, coroutineScope, launch, async, and suspend. Go's simplicity feels nicer to me.
- ackfoobar 4y ago`runBlocking` and `suspend` is the coloured function point. Some hate it, I don't mind it. It has its benefits. --- A quick search on SO gives this example of parallel decomposition. https://stackoverflow.com/questions/57662739/pattern-for-fetching-multiple-fields-in-parallel https://stackoverflow.com/questions/57662739/pattern-for-fet... Starting several `async` tasks, then awaiting them is cleaner for me. But that's subjective. --- Kotlin's structured concurrency handles the context stuff by default. When you handle those concerns (cancellation for example) in go, it's just as complex, but more verbose.
- rcme 4y agoGo's context object is used for cancellation. I think this is pretty simple and has the benefit of avoiding any type of exception handling while still giving you the ability to clean up anything you were doing. In general, if you like Kotlin, I can see why you'd like their approach to concurrency. Kotlin really likes a large standard library with generic blocks that act as syntactic sugar. I used to like that too, but as I've gotten older I've gotten lazier. Kotlin now how too much mental overhead for my old brain.
- tadfisher 4y agoThe coroutines support in the stdlib is here: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/ https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutin... You can build exactly Go's coroutines API on top of these primitives, by encapsulating the Continuation object in Channel and Mutex types, and you would not have to touch the `suspend` keyword (which is sugar for CPS-transforming the function).
- rcme 4y ago
- int_19h 4y agoThe other consequence, however, is that Go coroutines only work with other Go code, and bespoke Go threads and stacks require the runtime to jump through a lot of hoops to do FFI correctly, with the result that the ecosystem tends to rewrite rather than reuse existing code in C and other languages. In contrast, the approach with explicit promises and await can be mapped all the way down to a C struct with a function and a data pointer representing a callback. That is, it is C ABI compatible, and thus it can be easily composed across any boundary that respects that ABI.
- rcme 4y agoThe only runtime hoop I've ever had to jump through was to invoke runtime.LockOSThread() inside a go-routine. In a lot of ways: go func() { runtime.LockOSThread() // ... }() Is simpler than creating a thread in other languages.
- int_19h 4y agoThe runtime jumps through those hoops for you as needed, mostly, but there are consequences to that wrt performance.
- PaulDavisThe1st 4y agoThe cost of context switching between threads sharing the same address space is much lower than between tasks, because there's no need for a TLB flush. It just comes down to the cost of saving/restoring register state (more or less), and that's a nice known fixed cost on any given processor. Context switching between tasks has a variable cost, because the implications of the TLB flush depend on the working set size of the switched-to task. If it doesn't touch much memory, the variable cost is low; if it touches a lot, the variable cost can be much, much higher than the register save/restore (fixed cost).
- nordsieck 4y ago> It just comes down to the cost of saving/restoring register state (more or less), and that's a nice known fixed cost on any given processor. There's also the cost of cache eviction. Not something you have to manually manage, but it's a cost you pay nonetheless. Maybe.
- PaulDavisThe1st 4y agoIndeed, I ought to have mentioned the cost of cache misses post-context switch too. It too is working set size dependent, but the relationship looks different from the cost of TLB misses. However, my understanding is that an inter-task thread switch would also not cause cache invalidation (there's no need; the address space remains the same).
- bitcoinmoney 4y agoWould you need to flush a TLB on a regular context switch? That seems inefficient as threads are being scheduled per quanta in Linux. TLB entries are only evicted on page faults right?