24 ms·
Even in Go, concurrency is still not easy
- naikrovek 6y agoHmm, 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.
- 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.
- valenterry 6y agoGo just sells itself well. Languages that _actually_ make concurrency easy, such as Haskell, Scala, OCAML or distributed concurreny easy (Erlang/Elixir) are way ahead of Go but are not as hyped and are not marketing themselves as well.
- jshen 6y agoConcurrency isn’t easy in any language.
- Jtsummers 6y agoErlang comes pretty close to achieving this. When/if people get past the “weird” syntax it’s a very straightforward model that minimizes a lot of the hazards, though locks are definitely still possible.
- neilwilson 6y agoThe actor model struggles when you run up against something that is naturally sequential and the order matters. If you want a 1000 households to visit a set of shops in a repeatable Random order until the shops runs out of stock, that’s a List sort with a seed and a sequential loop Make the 1000 households concurrent and sending messages and it becomes a major exercise in clocking, scaling and scheduling.
- mikhailfranco 6y agoErlang processes (Actors) solve this kind of problem naturally, with code that is concise, asynchronous and parallelized. For example, look at solutions for 'Sleeping Barber'. It's possible to write a full solution in <40 SLOC. Starting 1000 processes is trivial and fast in Erlang. See the first few pages of Joe's presentation from 20 years ago: process creation time ~10us (up to 30k processes); message time ~1us [1]. The code is in his book and the email thread [2]: [1] https://www.rabbitmq.com/resources/armstrong.pdf https://www.rabbitmq.com/resources/armstrong.pdf [2] https://erlang.org/pipermail/erlang-questions/2007-July/028068.html https://erlang.org/pipermail/erlang-questions/2007-July/0280...
- axaxs 6y agoThe problem is defined, but all of the proposed solutions don't seem fitting. When using a semaphore, it's usually for a reason. I think the solution with the least overhead and perhaps most idiomatic is to spin the found reading loop in a goroutine BEFORE you start any at all, and leaving the rest of the code as is. This prevents having x number of idling goroutines, where x can be billions(but not in this case). Though again, in this case, using a semaphore and perhaps goroutines at all seems spurious.
- klodolph 6y agoMy personal style here is to use a number of goroutines equal to the desired concurrency. I know goroutines are cheap and there is no particular reason to conserve them most of the time, but this feels more natural to me than using a semaphore--although it is less flexible. Simplified: var wg sync.WaitGroup wg.Add(jobs) ch := make(chan workItem, jobs) for i := 0; i < jobs; i++ { go func() { defer wg.Done() for item := range ch { processItem(item) } }() } for _, item := range items { ch <- item } close(ch) wg.Wait()
- andonisus 6y agoHere's how I like to do it: https://play.golang.com/p/AB6exUIkNRg https://play.golang.com/p/AB6exUIkNRg Contents pasted below. package main import ( "fmt" "sync" ) func main() { wg := &sync.WaitGroup{} e := NewParallelExecutor(uint64(10)) for i := 0; i < 100; i++ { wg.Add(1) func(_i int) { //make closure to capture i correctly e.Submit(func() { fmt.Println(_i) wg.Done() }) }(i) } wg.Wait() fmt.Println("All done.") } type Executor interface { Submit(func()) } type parallelExecutor struct { queue []func() inCh chan func() doneCh chan struct{} } func NewParallelExecutor(limit uint64) Executor { pe := ¶llelExecutor{ queue: make([]func(), 0, limit), inCh: make(chan func()), doneCh: make(chan struct{}), } go func() { running := uint64(0) for { select { case <- pe.doneCh: running -= 1 case f := <- pe.inCh: pe.queue = append(pe.queue, f) } if len(pe.queue) > 0 && running < limit { running += 1 fmt.Println(fmt.Sprintf("Goroutines in flight, waiting: %d | %d", running, len(pe.queue))) f := pe.queue[0] pe.queue = pe.queue[1:] go func(_f func()) { _f() pe.doneCh <- struct{}{} }(f) } } }() return pe } func (p *parallelExecutor) Submit(f func()) { p.inCh <- f }
- inaseer 6y agoConcurrency is hard and we have very poor support for testing correctness of concurrent and distributed systems. Language abstractions help but they aren't nearly enough (as evidenced by this post). My team at Microsoft leverages Coyote to check the safety of our services against such subtle race conditions. We blogged about using it to reliably reproduce and fix a very subtle bug in a bounded buffer implementation over at https://cloudblogs.microsoft.com/opensource/2020/07/14/extreme-programming-meets-systematic-testing-using-coyote/ https://cloudblogs.microsoft.com/opensource/2020/07/14/extre... If you're using .NET in your projects, you can start taking advantage of such tools _today_. I would like for such tools and testing techniques to become more and more common place in the industry as concurrent and distributed systems are _hard_ and we should use all the help we can get.
- ereyes01 6y agoGo comes with thread sanitizer, which you can enable with go test -race ... If your unit test exercises a race condition, this will blow up your test with stack traces of the data race. It sounds a bit like Coyote, which also looks very useful for C# applications.
- inaseer 6y agoNeat to learn about thread sanitizer. It sounds similar to another tool from Microsoft Research called Torch (https://www.microsoft.com/en-us/research/project/torch/ https://www.microsoft.com/en-us/research/project/torch/) which automatically instruments binaries to detect data races. Coyote is similar in some ways but different in others. Coyote serializes the execution of the entire program (running one task at a time), exploring one set of interleavings and then rewinding, and then exploring another set of interleavings, hoping to hit hard-to-find safety and liveness bugs. In addition to finding concurrency bugs in one isolated process, we use it to find bugs in our distributed system by effectively running our entire distributed system in one process and having Coyote explore the various states our system can be in. It sounded mind-boggingly cool when I first came across this way of testing distributed systems through Foundation DB (https://www.youtube.com/watch?v=4fFDFbi3toc); https://www.youtube.com/watch?v=4fFDFbi3toc); we're emulating this kind of testing in our distributed system through Coyote. And unlike Foundation DB which had to develop their own variant of C++ to be able to do this kind of testing (kudos to them for doing it), Coyote allows us to do it on regular C# programs written using async/await asynchrony and benefit from decades of Microsoft Research in exploring large state spaces effectively.
- xmprt 6y agoMaybe I'm dumb but I don't understand the bug. I understand why the main code blocks if no tokens are available in the found channel but why would writing to `found` block the code (and why only in the case when there's a lot of go processes)?
- jeffbee 6y agoIt deadlocks if len(pss) > concurrencyProcesses.
- xmprt 6y agoEven in that case, wouldn't the main loop block for a short time but then unblock after the goroutine returns (and the defer function is executed)? I thought the whole point of limitCh is to support the case where len(pss) > concurrencyProcesses but in this case you're telling me that it's breaking things?
- ben0x539 6y agoYes, that's the whole point, but it's completely broken. I've certainly made the same mistake. :D
- jeffbee 6y agoThe inner goroutine cannot return because it blocks at found<-P, because found is not buffered and there is no reader. This would work if the consumer was started in a goroutine first.
- xmprt 6y agoThanks. The part about starting the consumer in a goroutine actually made more sense as a solution than either of the given solutions. I guess I completely forgot how channels work and didn't realize that until there's a reader, the channel will block (which makes perfect sense in hindsight).
- ben0x539 6y ago
- Animats 6y agoThat's a classic deadlock bug - two producer/consumer relationships in opposite directions. Have another goroutine receive and output the "found" results, rather than doing it in the first loop. Now you have a proper pipeline. The big win with goroutines is that they're green threads. They can block, and you can have lots of them. With regular threads, too many will use up resources. With "async", anything that blocks or just takes too long locks up the whole system. Go has the best of both worlds, which is very useful for the case of a server holding open connections to a huge number of human-paced clients.
- mathw 6y agoI've never worked with a green threads system in any real sort of environment where I was actually solving real problems at scale, but I always wonder, does the underlying runtime ever push them into actual real threads that can actually run on another core, or are they always the imaginary concurrency of using the gaps where another green thread needs to wait the comparative eternity for some I/O?
- vardump 6y agoYes, the runtime pushes them to actual threads as needed.
- _ph_ 6y agoBy default, a go program runs computations on as many threads as there are CPUs in your system (this can be adjusted via gomaxprocs). The execution of all goroutines is distributed amongst these threads. For maximum efficiency you want to avoid spawning much more threads than there are CPUs, as scheduling has an overhead and each thread consumes valuable memory for the stack space. The Go model of running many goroutines distributed across threads is a very efficient solution and also very flexible, as the program writer does not have to make any assumptions about the CPU count of the machine the program runs on.
- gpderetta 6y agoknow nothing about go, but surely the solution is using something like select?
- jacktang 6y agotry erlang!
- fulafel 6y agoWhat would a test for this look like, having a lot of processes?
- ben0x539 6y agoMaybe make `concurrencyProcesses` configurable and test with a bunch of different values relative to the input size?
- inaseer 6y agoThat's a great question. Stress testing, which is what you are suggesting helps, but is not super effective and often misses bugs. You need tools which can precisely control the task/go-routine scheduling during testing and systematically explore the various interleavings which can happen in the system. We generally don't have good tool support for such testing. There are promising tools emerging however; here is a case study of one such tool and how it was used to reliably reproduce and fix a subtle concurrency bug: https://cloudblogs.microsoft.com/opensource/2020/07/14/extreme-programming-meets-systematic-testing-using-coyote/ https://cloudblogs.microsoft.com/opensource/2020/07/14/extre...
- donatj 6y agoI’ve been wondering for like 15 years when a language is actually going to show up where I can just write my code procedurally and the compiler automatically figures out what it can parallelize. Like if I have 3 definitions in a row that set variables to the result of methods that don’t share memory, that seems pretty obviously parallelizable. Why isn’t anyone doing these sorts of optimizations? I would have thought it would have been solved by now, but I was wrong.
- smabie 6y agokdb+/q automatically parallizes operations in the most recent release, but since it's closed source, it's unclear how far it actually goes. It would be cool if the compiler could construct a DAG of operations and automatically offload independent computations onto different cores. I think this would necessitate a purely functional language, though.
- gonzo41 6y agoThat sound 'cheap' but if you think of the clock rate of a CPU, doing those tasks in parallel will take time to setup, process and teardown. That example doesn't really work well. Fitting problems into grids of processing like SIMD or GPU's is tricky because not a lot of problems fit those well like images. Most of what you're asking for is about understanding business flow which a language will never do.
- valenterry 6y agoBecause it doesn't help performance. It only really helps if you execute calculation on different threads - but then you need to prepare for these threads to fail (e.g. be killed by the OS) and to handle this.
- The_rationalist 6y agoBut with Kotlin flow it is!
- pcjthug 6y agoDid Go ever make concurrency easy? Any modern language allows you to create threads (semantically equivalent to goroutines) and pass messages between them. Most of them even have high-level concurrency support such as parallel map, structured concurrency, reactive extensions, type-safe generic collections... We could say that Golang makes concurrency using thread semantics scale to a large number of threads. When this paradigm is appropriate, then indeed Go is nice. The select statement is nice. But this is not always the right approach. Yes, it is true that go needs slightly fewer characters to launch a naked thread than in C# or Java. In that sense, it is easier. Anyone who considers that a major selling point is a walking race condition. As a tongue in cheek analogy, what if we consider my new language G, in which G is an alias for goto. G is going to finally make control flow easy. I've done away with complicated control flow constructs such as for or while - users wouldn't understand them anyway. Users are encouraged to use the G keyword liberally. After all, it's only 1 character. Indeed, by that metric, it's even simpler than go's go keyword. What could go wrong?
- navidr 6y agoESL here. What do you mean by “ walking race condition”?
- recursivecaveat 6y agoA "walking X" is a person who is or is going to create an X. For example, an unpleasant boss might be a "walking turnover problem".
- navidr 6y agoThank you for your detailed explanation. I really appreciate it.
- _ph_ 6y agoI do think that Go has a great set of tools to write concurrent programs. But I think it is a fallacy to believe that as a consequence "concurrency is easy" in a general way. These tools still require the programmer to be aware of the challenges of concurrency. They do make it more easy to deal with them. It is the same story as with garbage collection. Garbage collection prevents some kinds of errors and in general makes it way more easy to deal with dynamic memory allocations. However, garbage collections does not mean you don't have to think about allocation patters and object lifetime for example. Go does make concurrency much easier by the tools it provides. Most importantly are goroutines, which are very lightweight, so within reasonable limits, you don't have to be concerned about the number of goroutines you spawen - but as the example shows, you shouldn't try to spawn more goroutines as there are file handles available to your process, if every goroutine allocates a file handle. Not only are goroutines very lightweight with small growable stacks, but as they are part of the language specification, the compiler can generate code which helps with scheduling, in most cases you have efficient cooperative sheduling, so thread-based preemption is less frequent. On top of that, channels provide a very easy to use abstraction for communication between goroutines. There are a lot of use cases, where goroutines and channels give you very easy and safe concurrency. It does not save you from understanding concurrency issues and especially deadlocks. Which in a general sense is impossible to achieve, because to prove that a program is deadlock-free would require to prove that all goroutines return and that is equivalent to the halting problem, which is unsolveable. It should also not be underestimated, that due to goroutines and channels being part of the language spec, these are very commonly used features. Concurrency is present in most Go programs. As a consequence, when one reads a lot of Go code, there will be plenty examples of their usage. And any library needs to be thread-safe as the likelyhood is very high that it does get called from goroutines.
- deleted 6y ago[deleted]
- rad_gruchalski 6y agoThe protoactor library for golang: https://github.com/AsynkronIT/protoactor-go https://github.com/AsynkronIT/protoactor-go. I can’t recommend it high enough. It makes working with highly concurrent go so much easier and is a blessing for anybody with prior experience in Erlang or Akka.
- yencabulator 6y agoA more common way to write limited concurrency is to use a worker pool: https://blog.golang.org/pipelines https://blog.golang.org/pipelines
- atombender 6y agoWhen I first came to Go, I'd heard a lot of people speak profusely about CSP and how Go made concurrency easy. I was disappointed at just how not easy it was. For example, Go has no generic support for atomic primitives, arrays, slices, or maps. It has sync/atomic, but it's not generic. I always reach for Uber's atomic library, which has typesafe atomic wrappers such as atomic.Bool. They're so common, you'd think a CSP-aware language would provide keywords declare things atomic: var b atomic[bool] if cas(b, false, true) { ... } "Classical" Go didn't have errgroups, contexts or cancellation. To build something truly robust, "modern" Go ends up involving all three (plus atomics, of course). That's because almost all complex situations need to build what are effectively nested trees of goroutines that all need to quickly abort and unwind on errors or panics. But as you use more and more of these primitives, your code (or at least my code) becomes more and more obscured by the layers of error-handling, cancel-detection, retrying, and so on. I really wish some of this was built into the language, especially cancellation. For example, channels don't support contexts. So if you have a loop like this: for evt := range taskCh { handle(evt) } ...then you have to rewrite it to support cancellation. So you have a few options. One is to select on both the channel and context: for { select { case evt := <-taskCh: handle(evt) case <-ctx.Done(): return ctx.Err() } } Because of this, you can no longer use a for loop. Immediately the code got more bloated and less readable. Another option is to spawn a goroutine whose only job is to abort the channel: go func() { <-ctx.Done() close(ch) }() for evt := range taskCh { handle(evt) } return ctx.Err() That's a bit better (and you could move the for loop into its own function for clarify, without changing anything else). But it's still worse than a simple for loop! As a real-world example, here [1] is some code I've been working on lately. A controller starts N workers that need to process a task queue. If the controller is stopped, all the workers need to stop. So I use the pattern above. But I can't use a for loop! That's because I use a task queue abstraction that needs to support features that Go channels don't. I can't use a for loop or select block to block on the task queue, because only channels support that. So that's another problem with Go: Often, you can use raw channels as your data processing primitive if things are simple enough, but in practice, channels are best for coordination, and you have to build your own primitives — and yet, by building those primitives, you lose language expressiveness. To address the concrete example, I'd love to do this instead: // Range over task queue (custom object with blocking // semantics), automatically cancel if ctx is cancelled for task := range taskQueue with ctx { handle(task) } [1] https://gist.github.com/atombender/6bcff2c2d8fec32bc80ce1f57a8e61f6 https://gist.github.com/atombender/6bcff2c2d8fec32bc80ce1f57...
- apta 6y agoUsing goroutines in golang is not dissimilar from threads in other languages (except that you can spawn much more of them), with all the downfalls and gotchas. As a matter of fact, it's even worse than other languages with proper concurrency libraries and data structures (e.g. it has nothing remotely close to Java's `java.util.concurrent` package). What we see with golang is a phenomenon where people just parrot what some well known figures said at some point in time, without proper evidence or any basis (and in some cases, even when the evidence is counter to those claims). I'm looking forward to Java's green thread implementation (project Loom), as it has proper ways to manage cancellation and deadlines and hierarchies, all of which have are quite verbose and error prone, or not supported at all in golang.
- appleboy46 6y agoThe following is my solution: See the playground: https://play.golang.org/p/pXJaGQ0efe8 https://play.golang.org/p/pXJaGQ0efe8 Source Code is available on GitHub: https://github.com/go-training/training/blob/2ddb95d08c654a6410c2cfa70b0f506d8deceda9/example38-concurrency-is-still-not-easy/answer02/main.go#L1 https://github.com/go-training/training/blob/2ddb95d08c654a6...