7 ms·
Hmm, I don't know. I had this limited concurrency issue on the first Go program I ever wrote, and I'm very new to concurrent stuff, certainly if you only count
by naikrovek 6y ago
Hmm, I don't know. I had this limited concurrency issue on the first Go program I ever wrote, and I'm very new to concurrent stuff, certainly if you only count the past ten years or so.
I figured this out in Go about 30 minutes after I realized I needed to limit concurrency, and I'm a dumbass, basically.
So, I'm not sure that "still not easy" is accurate, especially given that I have a lot of trouble conceptualizing async and await, as used in C#. In fact, I don't think I've ever successfully used that paradigm in C#. I've been told that C# async and await are "easy" and this article says Go concurrency is "still not easy" and I've had the exact opposite experience in both cases.
In fact I really don't understand why async and await is even a thing anymore, given the paradigm that Go uses and the example it sets. It is almost supernatural in its ease, for me.
- pjmlp 6y agoThe paradigm that Go uses only fits a very narrow case of concurrency, and actually it is quite old. Modula-2, Concurrent Pascal, Active Oberon already had similar constructs. Async/await are configurable, not only can you replace the task scheduler algorithms, if the type being applied on does provide some magic methods, the runtime will use them instead of the default ones. This allows for very powerful concurrency optimizations and workflows, that still look like plain async/await calls.
- YetAnotherNick 6y agoAsync/await can easily be implemented through channels. I view channel as providing superset of functionality then async/await at least. Granted there are other paradigms like lock free synchronization which can not be directly be translated to channel.
- pjmlp 6y agoSo how do you change the scheduling algorithm though channels?
- YetAnotherNick 6y agoasync/await and channel are independent of scheduling. Nodejs doesn't have multithread but has async/await. I find goroutines lightweight enough in my personal testing and didn't tried tweaking it.
- pjmlp 6y agoOn .NET and C++ they surely are not, given that you can fine tune them either by providing new schedulers, magic methods on awaitable objects that can be either value or reference types. So not only you can control the scheduling algorithm, you can control how the async/await state gets managed in memory and also introduce additional parameters that influence the decision when to switch tasks across owner threads. Try to do this in Go via channels, while offering just the channel syntax to the caller. EDIT: The only thing in common between JavaScript, Python, .NET, C++ and Kotlin async/await is the name of the primitives.
- unscaled 6y agoCustom stackless coroutine schedulers are also available in Kotlin. Python also also allows you to customize the event loop, I think, but overriding AbstractEventLoop. A M:N implementation would probably be inefficient though because of the GIL. Basically almost all stackless implementations I know allow for custom scheduling. The fact that some of them can't do M:N and are restricted to 1:N is just a limitation of dynamic programming languages. There is a global interpreter state which prevents you from using multiple OS threads efficiently. You either rely on GIL (CPython and Ruby MRI) or rely on just allow only one thread (Node, Lua). So GP is right in that not _all_ async/await implementations will allow you to fully customize your threads but I think the main point here is that stackless coroutines are practically _a prerequisite_ for enabling custom scheduling (like C# et. al allow). You cannot have this custom scheduling with stackful coroutines, because that would require the scheduler to have access to low level memory management for controlling the stack. I've never seen a language which allows that. I guess this could be doable on C++, but I really don't see the point, since it will require a lot more effort than the current stackless C++ coroutines.
- unscaled 6y agoSomething that looks like async/await and has a rather bad performance can be implemented through channels. Nestable stackless coroutines cannot. By definition they require compiler support. Your proposed solution will not have any of the advantages of stackless coroutines (effect typing of async functions and perfectly optimal memory use) while suffering all the performance penalty of running on top of a channel.
- YetAnotherNick 6y agoChannels have close to zero performance penalty. If you are talking about goroutines, you need to come up with data or code. I found goroutines to be really cheap to create.
- unscaled 6y agoNot true: https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-should-feel-bad/#channels-are-slower-than-implementing-it-yourself:7de476e370ca6a780a51fd680c5a51dd https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-s... I'm pretty sure the implementation has seen some improvements since then. But using a channel for async await is the same as using a global mutex every time you await. That is bound to be contentious on high load.
- Taek 6y agoI think today channels cost a few hundred nanoseconds. For most applications, this is 'close to no overhead'. If you are doing a really tight loop of course or have threads that bump into eachother a million times per second, you're going to want a different primitive. But most programmers aren't going to see any overhead using channels instead of something else.
- unscaled 6y agoPlease remember the approach is "every context switch should go through a centralized channel". This including waiting on all other channels. I need to see some benchmark to this level to be proven the overhead is negligible. And of course, there is still the question of WHY would you ever want to do that. What can be gained from simulating an async-await model on top of a channel.
- jshen 6y agoI’ll bet your Go code, if it isn’t trivial, has race conditions all over and you just don’t realize it.
- papaf 6y agoGo has a built in race detector that most teams run during CI. [1] https://golang.org/doc/articles/race_detector.html https://golang.org/doc/articles/race_detector.html
- xmprt 6y agoCan't that be said for most languages? Is there anything special that other languages do to make race conditions less likely than they are in Go?
- IshKebab 6y ago> Is there anything special that other languages do to make race conditions less likely than they are in Go? Yes, other languages provide you with more tools than the basic nuts and bolts. For example a language might provide you with a "parallel for" primitive that is well tested. The issue is not exactly that Go's concurrency stuff is bad, it's just that it doesn't give you much help so you have to implement it all yourself. Also the lack of generics means it is difficult to write libraries for this stuff.
- pansa2 6y ago> I really don't understand why async and await is even a thing anymore Programming with threads is hard because they can context-switch at arbitrary points. Goroutines are nothing more than (lightweight) threads. Stackful coroutines (as in Lua) aren’t much better, because they can context-switch at any function call. Code that uses async-await is different - it can only context-switch at points explicitly marked with `await`.
- deleted 6y ago[deleted]
- diek 6y agoasync/await in the general sense doesn't have anything to do with the preemption policy of a particular language implementation. For instance, C# implement async/await as keywords on top of its existing concurrency model. That's really what it comes down to: cooperative multi-tasking vs preemptive multi-tasking. Windows 3.1 had cooperative multi-tasking. Everything went great until one program didn't yield control, and then the whole system ground to a halt. That should sound familiar to any Node.js developers with their event loop. Cooperative multi-tasking also extremely complicates code that actually uses the CPU. If you look inside libuv, it even has to jump through hoops with its encryption functions by effectively calling yield() in between rounds of calculation.
- unscaled 6y agoSince stackless coroutine schedulers can often be customized, you could (and are encouraged to) launch coroutine code doing heavy performance on a thread based scheduler.
- csharptwdec19 6y ago>If you look inside libuv, it even has to jump through hoops with its encryption functions by effectively calling yield() in between rounds of calculation. I would have assumed the Yield was there to help with protect against timing pokes...
- jayflux 6y ago
- ben0x539 6y agoI think that's just "easy"/"not easy" from a different baseline, not a comparison of C# vs Go. C# async/await is easy compared to something like callback-based asynchronous IO. Go concurrency is "not easy" compared to serial code.
- mathw 6y agoasync/await isn't really concurrency. It's syntactic sugar over callback hell, which can be used to write concurrent software in the "spawn something, wait for it to finish later" model. It has absolutely nothing to help you passing data between ongoing threads, accidental unlocked access to shared data etc. etc.
- unscaled 6y agoThis used to be my assumption long ago, but this is incorrect for almost every implementation async await. The compiler does not syntactically converts your awaits to callbacks - it converts everything into state machines.
- thelazydogsback 6y agoC# converts to state-machines, F# converts to nested callbacks. (Although this may change, or be optional.)
- unscaled 6y agoInteresting. The only other instance I remember of (sort-of) awaits being converted into callbacks are LiveScript "backcalls": https://livescript.net/#functions-backcalls https://livescript.net/#functions-backcalls
- unscaled 6y agoCalling the stackful coroutine model as offered by Go superior to stackless coroutines ("async/await") is highly controversial. Stackful coroutines are not new - it might surprise you to know that they are older than async/await, existing in full form probably introduced at least in 1967, with the first documented implementations starting around 1958. Stackful coroutines were implemented in a wide array of popular languages in the 1980s as pjmlp noted. Stackless coroutines need better clarification, since the paper that defined this term [2] refers to a limited subset of stackless coroutines that we would nowadays just call "generators". To the best of my knowledge, when the paper came out generators were the only type of stackless coroutine in existence, and thus came the perception that stackless coroutines cannot be nested (they can) and that stackful coroutines are strictly superior to stackless coroutines (they aren't). You can see an example discussion here: https://news.ycombinator.com/item?id=16318535 https://news.ycombinator.com/item?id=16318535 The real difference is not nesting, but the fact that stackful coroutines manage a growable stack dynamically at runtime, while stackless coroutines pass the job on to the compiler to evaluate all possible nesting permutations, and create an optimally memory-efficient stack machine that represents the coroutine. To summarize the pros and cons of each, I think you can say: Stackful pros: * [No colored functions](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...) - summed up it says: I can write asynchronous I/O code the same way I write multi-threaded code or non-blocking code. It's definitely more ergonomic, but also means your code can hold more surprises for you. Not everybody thinks 'Colored Functions' are an absolute advantage, otherwise we wouldn't have Effect Typing. * Less GC allocations: stack is [re-]allocated in bulk every time time a lightweight starts (go statement) or whenever it needs to grow. Stackless coroutines typically allocate a smaller object on every call to an async function. Stackless pros: * Clear yield points: you always know the points where your coroutine could suspend and control would be moved to another coroutine: whenever you see an await statement[3]. This is why function colors are needed - to be explicit about the control flow. * Less memory waste and less overall allocation size: the "compile-time stacks" generated by stackless coroutines are perfectly efficient. There is no unnecessary memory allocated. I don't think it is clear that one model is strictly superior to the other, but I do believe that if you're aiming for ease of correct use (rather than ease of use), the stackless model is clearly better. It is definitely somewhat harder to wrap your mind around, but it is also somewhat harder to get a deadlock or have a hard-to-detect data race in this model. [1] Simula 67 (and probably Simula I too), and first implemented in Assembly as early as 1958 by Melvin Conway of Conway's Law fame. [2] http://www.inf.puc-rio.br/~roberto/docs/MCC15-04.pdf http://www.inf.puc-rio.br/~roberto/docs/MCC15-04.pdf by Roberto Ierusalimschy, the author of Lua. [3] It's slightly different in an awaitless language like Kotlin - here you actually have to check whether the function you're calling is suspendable or not.