10 ms·
Go Things I Love: Channels and Goroutines
- rubyn00bie 7y agoI guess I wish this was a bit more in-depth as to what "go" can do with channels or goroutines, but maybe I've just been using languages where all this is already possible. The article is a nice cursory glance, I just want to learn more :) I mean the first example it looks like is using a Mutex, and then (b)locking on it, and the second just looks like having a queue of messages (mailbox) that it rips through (like the actor pattern). Some questions I've got after reading this... How does the go-runtime (?) schedule these calls? Does it manage an internal thread pool? Is the scheduler asynchronous, parallel, or both? How do you manage contention or back pressure if I begin to flood messages to to one channel (or many)? How many channels can I have open and what are the limits? Can I still lock the system stupidly using channels, if so, how (or how not)? Edit: Truly, I'm curious because as I researched asynchronous programming and efforts to better future proof my career (years ago) as we began really increasing core counts-- Go never stood out. It's a fairly practical language, yes, but if I want a better paradigm for asynchronous programming the future it really isn't there (IMHO). BEAM stood out as something unique, the JVM stood out as something even more practical, and Rust stood out as something performant (with the serious caveat of not being C or C++), while Go has always seemed like an odd one to me... People talk about channels and goroutines like their special but they seem pretty damn run of the mill to me... WAT AM I MISSING?
- gjs278 7y agoyou manage the channel backlog with semaphores and wait
- SamReidHughes 7y agoGo channels have a fixed capacity, by default zero, and their main special feature (compared to just having some concurrent_queue<T> type) is the select statement, which helps devs do things the right way. Nothing stops you from stupidly locking the system, e.g. by having a thread block itself by writing to the channel it’s responsible for consuming.
- jayd16 7y agoIs the select semantically different than something like this? bool TryWrite(T) How does go handle cancellation with the select syntax? I guess you have a cancel channel and select across both the cancel and the read channels? Is Go smart enough to sleep and not busy wait in that case?
- TheDong 7y ago> Is the select semantically different than something like this? Yes. select can do a lot of things. It's overloaded to be something like 5 different things. Here, let me show you: chanOne := make(chan int) chanTwo := make(chan int) ctx := context.Background() // Receive whichever is ready first. Blocking select { case v := <-chanOne: case v := <-chanTwo: } // Non-blocking read select { case v := <-chanOne: default: } // Timeout and cancellation select { case v := <-chanOne: case <-time.After(1 * time.Second): // timeout case <-ctx.Done(): // cancel } // Write or read, whichever channel is ready first select { case chanOne <- 1: case v := <-chanTwo: } Another form of cancellation is closing channels, which causes reads to return the zero value immediately, and causes any writes to that closed channel to immediately panic (effectively abort the program).
- jayd16 7y agoNeat. Most of the permutations are easily handled in other languages as well except for the full blocking (sleeping) case. Without a channel the scheduler can reason about, most languages would have to busy wait or possibly pull from multiple channels.
- SamReidHughes 7y agoI think the biggest advantage is that it's a lot harder to screw up using a select statement than most equivalent API's you'd create in other languages that allow waiting on one of N channels/signals/channel/event operations, and branching based off that. Simply because you're either constructing a bunch of callback objects or you get some number N saying the Nth parameter had an event, and your code has to match N to the specific channel correctly.
- Vendan 7y agoGoroutines are scheduled onto system threads via M:N greenthreading. Each "system" thread can steal work from other threads, and there's some stuff so that blocking system calls don't use up your "goroutine system threads" (you can put a limit on the number of goroutines that run concurrently). Channels are fixed size on creation, either 0 (directly handing off items from 1 goroutine to another) or >0 (the channel has space allocated to hold that many items). If the channel is full(buffered, i.e. >0 size channel) or there's nothing waiting to receive the item(unbuffered channel), the sender sleeps until it can send the item. Channels are purely data structure, have as many as you want. You can lock the system fairly trivially, but Go can detect it in many cases and give you stack traces of all the places stuff is waiting to read/write (only if all goroutines deadlock). edit: And no, there's nothing really special about channels, just they play nice with the goroutine scheduler, so it's perfectly sensible to do lots of "wait on these channels" stuff inside your program, without having to have lots of OS threads and such. (goroutines are a bit more lightweight then OS threads)
- lmm 7y agoIt's like what you'd do with actors on top of some kind of userspace scheduling (e.g. a thread pool that accepts tasks), yes. Writes to channels are implicitly yield points, so you write cooperative multitasking code (again, like you'd do in any language with userspace scheduling) with all the advantages that entails (no blocking), but without having to explicitly manage your sequencing/yielding. Since the whole language is built around the idea that this is going to be happening, a lot of the immediate problems with userspace scheduling go away: anything that's trying to do thread-local storage (e.g. web requests or database sessions getting bound to the current thread) will necessarily be goroutine-aware and do the right thing. There's no free lunch, of course: it becomes much harder to write code that is pinned to a single system thread (e.g. good luck interoperating with a C library that expects you to pass callbacks), or to use higher-performance blocking I/O in the cases where that's warranted. But it makes the 80% case very straightforward.
- _tkzm 7y agohttps://www.youtube.com/watch?v=h0s8CWpIKdg https://www.youtube.com/watch?v=h0s8CWpIKdg
- cle 7y agoThe example under "Communicating by sharing memory" isn't correct, despite the author claiming that "it works". It's a very common example in concurrency 101 (updating a value). The fact that the author claims that it's correct is pretty concerning to me. Adding a print(len(ints)) at the bottom of the function: $ go run test.go 5 $ go run test.go 8 More on-topic, channels have their own tradeoffs. I often reach for WaitGroups and mutexes instead of channels, because things can get complicated fast when you're routing data around with channels...more complicated than sharing memory. I don't think it's good advice to broadly recommend one over the other--understand their tradeoffs and use the right tool for the job at hand.
- leCapitalist 7y agoEdit: removed (can’t seem to delete?)
- harikb 7y agoNo, GP was taking about a clear race condition on growing the slice in the first example. It is not just a case of "idiomatic go" vs "idiomatic" or something, as the author suggested, a problem only when the code grows. It is a critical bug in the first example. Edit: add what one gets when run with `go run -race` > WARNING: DATA RACE > Read at 0x00c0000a6000 by goroutine 8: > runtime.growslice()
- deleted 7y ago[deleted]
- Carpetsmoker 7y ago> More on-topic, channels have their own tradeoffs. I often reach for WaitGroups and mutexes instead of channels, because things can get complicated fast when you're routing data around with channels...more complicated than sharing memory. I don't think it's good advice to broadly recommend one over the other--understand their tradeoffs and use the right tool for the job at hand. Unfortunately, some Go people reach for the "this is not idiomatic"-cudgel far too quickly, instead of actually looking at the various trade-offs.
- mathw 7y agoI do like that select statement which hits the first case that has its channel ready with a message. That's very nice. And having channels in your standard library is brilliant and everyone should do it. A shame about the shared memory thing though. I firmly believe that designing a language where memory is shared by default is a Bad Idea. You should probably provide a way to allow it when you really need it (for performance, usually, in very very very carefully-designed code), but having memory sharing by default is a source of soooooo many bugs. I know, because I've caused most of them.
- pojntfx 7y agoChannels are really nice; I love writing "workers" with them and sending errors and "status messages" with a simple `status <- "starting supernode"`/`errors <- err`; doing this with i.e. Node's async/await is just so much more complex.
- osrec 7y agoFor anyone interested in using channels and coroutines in PHP, swoole (https://github.com/swoole/swoole-src https://github.com/swoole/swoole-src) provides a reasonable implementation!
- _pmf_ 7y agoGo channels are nicest concurrency mechanism I know. They bring nice ergonomics to the conceptual simplicity of a select()/epoll() loop.
- coder006 7y agoApart from the main topic, really liked the layout and theming of your blog. Curious to know of it's hosted somewhere or self built.
- iamjfu 7y agoThanks! It's a customized hugo theme https://github.com/justindfuller/hugo-theme https://github.com/justindfuller/hugo-theme that I forked from https://github.com/alanorth/hugo-theme-bootstrap4-blog https://github.com/alanorth/hugo-theme-bootstrap4-blog
- meddlepal 7y agoI've found channels create more complexity than their usually worth and it's often simpler, more readable, and more maintainable to just use a sync.Mutex or sync.RWMutex.
- shakezula 7y agoI'd be hard-pressed to disagree. The entire time reading this blog post, I was confused why a plain function couldn't have been used instead.
- apta 7y agoThe only thing golang has going for it is "goroutines". Now that all other popular languages are getting some variant of async (e.g. C#) or green thread implementations (e.g. Java), it will be tough to advocate for golang for new projects given its severe shortcomings.
- Thaxll 7y agoOther language do it completely differently, there is no such thing as async in Go since the paradigm is blocking from a programmer perspective. I don't think understand how it works in Go.
- apta 7y agoI know how it works in golang. It's more or less transparent to the user, just how like it's being implemented in Java where the user doesn't designate functions as `async`, and the runtime automatically takes care of it.
- theflyinghorse 7y agoAs an aside Java's concurrency is outright confusing and just plain hard to get right. Doing concurrency in golang was like a breath of fresh air.
- apta 7y agoWhat's confusing about it? It's built on threads and mutexes, not unlike golang with its goroutines and mutexes. The main difference is that Java will be getting green threads soon, which will allow the user to spawn hundreds of thousands of them if he/she wants to.
- theflyinghorse 7y ago> What's confusing about it? Do I use Thread, Runnable, Executor, ExecutorService, or CompletableFutures? How do they interact with synchronized and Locks and which Locks do I use? Where do Semaphores fit in this picture? Sure, for someone who is in Java day in and day out and who deal with concurrent java all the time these might be very straightforward tools with clear separation of goals and intent. In my personal experience there are always easy to introduce bugs with Java's concurrency, and every time I have to do something concurrent in Java I reach for my notes first trying to refresh my memory on what's what, which I don't need to do with golang.
- dickeytk 7y agooff tangent, but I really like that syntax of defining types especially for channels: type Foo(chan<- int) instead of what I usually see type Foo chan<- int unfortunately it doesn't appear compatible with gofmt (entirely), which changes it to: type Foo (chan<- int) I still think it's a good pattern for channels though. It makes it a lot clearer what the type is especially if you have a slice of channels: type Foo []chan<- int vs type Foo [](chan<- int)