Go Things I Love: Channels and Goroutines
justindfuller.com
justindfuller.com
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.Edit: add what one gets when run with `go run -race`
> WARNING: DATA RACE > Read at 0x00c0000a6000 by goroutine 8: > runtime.growslice()
Unfortunately, some Go people reach for the "this is not idiomatic"-cudgel far too quickly, instead of actually looking at the various trade-offs.
They are not universally a bad thing, but total avoidance of sync package primitives is a bad idea (and much slower in many circumstances)
This gives a good analysis:
https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-s...
> Now that goroutine needs to be closed, then you end up with an additional close channel
Goroutines that just map values or similar will usually share a context with some other goroutine and can therefore reuse that context's close channel.
So, having a context is still "end[ing] up with an additional close channel" as silasdavis said. Even if you pass it to something that will be cancelled like a network operation, you still must correctly notice and handle the resulting timeout error.
The value for context is almost entirely that everybody in the community has come to agree on it rather than its functionality per se. Which is the same as io.Reader, in that the utility isn't its functionality, which any number of languages can replicate, but the way everybody agrees to implement and use it through the entire ecosystem, which is where other languages have a lot more trouble. Nothing technically stops that from happening, but you often end up with islands of agreement in different major frameworks or library ecosystems instead of ecosystem-wide agreement. Context is everywhere now; it's actually been a few months since I encountered a place I wish that took it that didn't.
You would need to be careful that once one goroutine has sent a pointer to a channel, it doesn't touch that buffer again (until the receiver has finished with it, at least), otherwise you can get data races. You're implementing ownership semantics here, and unlike in some languages i could name, you're doing it without help from the type system.
> 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
You're absolutely right. I certainly didn't intend to give a blanket recommendation. It's more of a, "If you're sharing memory, might it become clearer if you share memory by communicating?" I was worried that the simplistic examples would not properly represent the cases I was thinking of. I think that's a communication error on me.
If I run it locally it's not consistent:
go run lock/main.go
[3 0 1 2 5 4 6 9 7 8] 10⏎
go run lock/main.go
[1 5 6 7 8 0] 6⏎
go run lock/main.go
[0 3 1 2 4 5 6 7 8] 9⏎
Here's a version that does work for me: https://play.golang.org/p/b6bRb9pgIGZThe issue is that appending to slices concurrently is not safe, so you have to use a lock around the append or similar.
I found this helpful: https://youtu.be/29LLRKIL_TI?t=1340
Also that for most uses a concurrent map is way overkill, and a thread-safe one is both costly and basically useless (hence the Java folks not keeping the thread-safety when migrating from Hashtable to HashMap).
On the other hand they're kinda shit given how awful non-builtin data structures are in Go, and how easy it is to "leak" maps between goroutines.
> Map is like a Go map[interface{}]interface{} but is safe for concurrent use by multiple goroutines without additional locking or coordination. Loads, stores, and deletes run in amortized constant time.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
I'm a beginner at rust, but this is the version I came up with that compiles and works:
https://play.rust-lang.org/?version=stable&mode=debug&editio...
Be careful relying on the results of the Go playground; it has a bunch of differences and probably has GOMAXPROCS set to 1, which most other systems will not.
You need a sync.Mutex or similar protecting your call to append :)
var wg sync.WaitGroup
n := 10
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
defer wg.Done()
doSomething()
}()
}
wg.Wait()
You still have to call wg.Done(), unfortunately. People have written small wrappers around sync.WaitGroup that change the interface: var wg bettersync.WaitGroup
n := 10
for i := 0; i < n; i++ {
wg.Go(func() {
doSomething()
})
}
wg.Wait()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?
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?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).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.
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)
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.
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.
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)I don't think understand how it works in Go.
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.
It depends on what you need. Executor is an interface, so you can't use that directly, you have to use one of its implementations (like ExecutorService).
CompletableFuture is what you use when you want to handle the return type of a given function or method call. I've come across so much golang code where a function that returns an error is invoked with "go foo()", and the error is ignored.
The thing is, when you use golang, you have to re-write all these abstractions yourself anyway. Want to have a function execute in the background then you wait on the value? You have to pass in a channel and wait on it. What if it now returns an error? You have to pass in two channels now, or return a composite struct. In Java, you'd use a CompletableFuture and be done.
Semaphores are similar to WaitGroup in golang (but with more use cases obviously).
You get much more control, as well as writing code with a clear intent when you use its concurrency abstractions. In golang, you're writing much lower level code that you have to dig in to understand what's going on.
Not to mention that in golang it's difficult to provide anything that comes close to what's in the java.util.concurrent package, mainly due to the lack of generics.
It's just a different perspective of concurrency than node.
I literally quote from go by example: https://gobyexample.com/goroutines
"Our two function calls are running asynchronously in separate goroutines now."
In relation to Golang: https://yizhang82.dev/go-and-async-await
You should know that the author of that article failed to mention that node is free of race conditions caused by context switching, this is a huge deal and in many cases worth the trouble of async await syntax.
The choice of synchronous or asynchronous communication have their own set of tradeoffs, but in most cases the additional complexity introduced by async is IMO not really work it.
However, channels were way overhyped, and one really should think long and hard about wether to use them at all, ie. find specific use cases. They're based on Mutex, but slower and often end up becoming a more complex distributed pattern.
Why not use channels? While sharing state/memory by communicating sounds nice, in practice you end up with coding requirements for a distributed system. Do you need one? Fine. It could even work out nicely for making a skeleton distributed monolith, before splitting into microservices.
But for most common use cases, channels distribute behaviour across your system unnecessarily, and you end up needing to synchronize and clean things up between different moving parts. If you don't have clearly defined bounded contexts such a pattern can fit into, it just seems like increasing complexity for no good reason.
Of course, for very straightforward implementations that you know won't grow in complexity, channels are fine, especially for simply reusing a working pattern.
In Java, if it gets first class green threads...does all code immediately start using those instead of an actual thread pool? Probably not; your runtime behaviors would change. Will all libraries immediately change to using them? Assuredly not, again, runtime behaviors will change, and no matter how cleanly implemented, switching and testing will take effort, for both the library maintainer and the consumer.
As a consumer, then, you're left with a moving target as some of your underlying libraries make the switch, plus the related testing effort. And you also have to either make a clean switch over, or you have to mix threaded + greenthreaded libraries, which is likely non-trivial.
Languages that offer a single, sufficiently powerful concurrency construct avoid that; all golang libraries support goroutines (even if they are not themselves leveraging them).
Yes, but any frameworks that currently create and manage a threadpool will not automatically be changed. As well as any libraries/frameworks that rely on async code. And any existing code you own will also not.
For brand new projects, using only libraries that assumed blocking code, and had no threadpool expectations within them, sure, it's transparent.
For any old projects it's not. For any libraries that were written to be async already, you don't benefit. For libraries written to manage a threadpool, you have to wait for them to be updated. And still, as now, God help you if you want to mix and match, to use a library written to be async, and a library written to use a threadpool...but of course, now compounded because you want to write sane, performant, readable blocking code using green threads. You have to deal with all those incongruities and make them all work together.
Languages that have an M:N threading mechanism from the get go (hah!) get the benefit of everything building atop that from the beginning.
Even with mixing and matching, current systems can already run thousands of hardware threads, so I'm not sure it will affect performance or debuggability in any major way.