This is the third of my posts about looking at Go from the perspective of a C++ developer. I think that will be enough for now. Here are all 3 parts:
Here I mention Go’s built-in concurrency features, letting you write code that makes the best use of one, or more, processors. I am not an expert in concurrency, and I think few people are. I’ve had some really helpful friendly feedback about the previous parts from Go developers, so I hope that continues. I will gladly correct any mistakes that are pointed out to me so the text can become more useful.
However, the mentions of C++ coroutines here might also be particularly useless. Clang and gcc don’t support coroutines in their stable releases yet and it’s hard to find definitive explanations. What I can find is contradictory and far from as simple as Go, probably because the C++ coroutines specification has been in flux for a while. I would very much welcome corrections. Interest in C++ coroutines seems to be heating up now so I guess this will settle down soonish. I think it will then be time for some books to be updated. I hope that the end result is at least as expressive as Go but I am not confident of that so far.
As before, I strongly suggest that you read “The Go Programming Language”, (referred to her as “the book”), from which I’ve learned this stuff. At least at first, the structure of this text is aimed at people familiar with C++ who are interested in Go, but the comparison does break down later.
Goroutines and std::task
Like C++ (since C++11), Go has support for simple concurrency features such as lockable mutexes (which we’ll see later), but it doesn’t let you create raw threads. Instead if offers goroutines.
Goroutines are, at their simplest, functions that can run concurrently with other functions. Two goroutines might even run on different CPUs, meaning that they run truly simultaneously. You can start one just by using the “go” keyword before a normal function call. The function will then execute, without the caller waiting for it to complete.
go doSomethingThatTakesALongTime() doSomethingElseAtTheSameTime.
While the goroutine does its work, maybe using traditional cooperative multi-threading on the same CPU, maybe on another CPU, the implicit main goroutine can also do work. Update: But careful. As mentioned later, this program won’t wait for the first goroutine to finish if the main goroutine finishes first.
So far, at least, this is a bit like starting a std::task in C++ with std::async() (since C++11):
auto future = std::async(doSomethingThatTakesALongTime) doSomethingElseAtTheSameTime() future.wait();
(Update: As James pointed out in a comment, if you don’t keep the returned future, its destructor will run, which will cause a wait, making the call not async.)
Hybrid Threading (m:n Threading)
A std::task in C++ is not guaranteed to start an actual new OS (kernel) thread, unless you pass std::launch::async as an extra argument. It will sometimes, but that’s up to the runtime and the implementation. I think the runtime, in some implementations, might even be able to move tasks to real threads and between threads, but I’m not sure. Scott Meyer’s “Effective Modern C++” has some good discussion of tasks and C++ concurrency in general.
Goroutines are not ambiguous about this. Goroutines don’t map directly to kernel (OS) threads. They use m:n threading, in which user space (the Go runtime, in this case) schedules its own m “threads”, mapping them onto a pool of n kernel threads. This seems to be what is referred to as “Hybrid Threading” in Robert Love’s “Linux System Programming” book – a mixture of 1:1 Threading (just using kernel threads) and N:1 Threading (or User-Level Threading, apparently sometimes called “green threads” or “lightweight threads”.). Presumably there is some very clever scheduling code in Go’s runtime and in the C++ coroutines implementation.
Because Goroutines don’t map directly to real threads, they don’t need the full stack size given to a kernel thread, letting us comfortably use far more goroutines than we would use threads. However, you only get the full benefit of this when the work is happening purely in the Go runtime. The book explains that “Goroutines that are blocked in I/O or other system calls or are calling non-Go functions, do need an OS-thread”. So I guess you still wouldn’t want, for instance, a goroutine per network connection, if you are serving many network clients.
Channels: Getting One Result
In C++, std::async() returns a std::future. This is a way to later wait for the result from the task, or just get the result if it is already ready, having made good use of the original thread’s time in the meantime.
int doSomethingThatTakesALongTime() { ... return 5; } auto f = std::async(doSomethingThatTakesALongTime) doSomethingElseLengthyAtTheSameTime() auto answer = f.get()
The go keyword doesn’t return a future. (Update: And doesn’t let you use the return value.) You could use the function’s result, but you’d just immediately start waiting for it, defeating the purpose (I wonder why Go allows this at all.)
Instead, to provide a result, a Go program would use a Go channel. Go channels are queues of your chosen elements. Channels are a built-in type, so they are generic – they can contain various types. They can be created like so, using the make() built-in function as for other built-in types:
c := make(chan string)
The Go language has special syntax for sending to, or receiving from, channels via the <- and -> operators, though I don’t see what would have been so bad about having send() and receive() methods.
Go channels are “concurrency safe”, so you can pass one to a goroutine, and both goroutines can happily manipulate it. For instance, to get a response to a long-running function, but only after we’ve tried to make good use of the waiting time:
func doSomethingThatTakesALongTime(c chan int) int { ... c <- 5 } c := make(chan int) go doSomethingThatTakesALongTime(c) doOtherStuffInTheMeantime() answer := <- c
That attempt to receive from the channel (with the <- operator) will block until the channel has something to provide, a bit like how std::future::get() blocks until the result is ready.
It would be fairly easy to create a future type that uses a channel, then you could write an async method that uses the go keyword and returns the future. But I think it would be nice if Go made this simple case simpler by doing this for us. Maybe it does.
I’m also a little uncomfortable with the calling goroutine (the main goroutine here) having to create the channel, repeating the type of that channel (it’s also in the called goroutines function signature). I think it would be nice if there was some syntax that gave us a new instance of the channel (Ready for receiving. See about unidirectional channels later.) that the called goroutine expected to get, while also allowing us to use an existing channel if necessary. That would be hard to get right, of course.
If the channel is empty and has been closed then it will then provide a zero value immediately. Or you can get the extra result to check if the channel is still open or still has values:
answer, ok := <- c if (!ok) { // The channel is empty and has been closed. }
Goroutines and C++ Coroutines
Goroutines feel a little like C++ coroutines (not yet in C++ as of C++17). I don’t know if their scheduling and stack management is quite as clever. I suspect not, because I keep reading stuff about C++ coroutines being “stackless”, which I think means that they map directly to kernel threads, while Goroutines are apparently “stackful”.
Goroutines allow cooperative scheduling, via blocking channel sends and receives (see select) and sleeps. So do C++ coroutines, via co_yield, co_return, co_await, futures and generators.
So I’ll mention C++ coroutines later as I show other features of Goroutines and channels. I’d love to know the actual differences, if someone can explain it to me clearly enough that I understand. I really hope the end result is as easy to use as Goroutines.
C++ Coroutines: Getting One Result
For now, let’s look at how a C++ coroutine would let us return a single value, much like we did with std::async() (with a std::future) and with Go’s go keyword (with a channel). We would use a std::future return type for the function’s implementation, use co_await to launch the function, and co_return to return the result into the std::future.
This is based on various other blog posts and slides that I’ve seen. I don’t know if this is really correct because I don’t have a compiler that actually supports this yet.
std::future<int> doSomethingThatTakesALongTime() { ... co_return 5; } int main() { int answer = co_await doSomethingThatTakesALongTime(); doOtherStuffInTheMeantime() // Attempting to get the value blocks until the result is ready. // (I'm not sure about this part. Do we need to explicitly wait // for the future to have a value?) std::cout << "answer: " << answer << '\n'; return 0; }
Here the co_await keyword is a little like the go keyword in Go.
Channels: Multiple Results
But channels are a queue of values, not just single values, so you could receive a series of results. Go’s range-based for loop has built-in support for this, as mentioned in section 8.4.2 (Pipelines) in the book. For instance:
func sendMessagesToOtherSolarSystemsAndGetResponses(c chan string) { for i := 0; i < 10; i++ { ... c <- response } c.Close() } c := make(chan string) go sendMessagesToOtherSolarSystemsAndGetResponses(c) doOtherStuffInTheMeantime() for response := range c { fmt.Println(response) ... } fmt.Println("Not waiting any longer for more responses.")
Notice that, unlike a regular range-based for loop in Go, this gives you the value, not the index (which would be meaningless for a channel). In a regular range-based for loop, you need to get the index and value (maybe using _ for the index) to get the value.
The range-based for loop blocks at each operation, waiting for a new value from the channel, and it will stop automatically when the channel is empty and closed.
As mentioned in section 8.5 (Looping in Parallel), if we return from the main goroutine early, for instance in response to an error, we must make sure to “drain” the channel if we don’t want the other goroutines to be blocked when they try to send, even when using a buffered channel (see below). You could launch a separate goroutine just to do this.
C++ Coroutines: Multiple Results
With C++ coroutines, to return multiple results, we can use co_yield instead of co_return, using a generator instead of a std::future.
Again, this is based on various other blog posts and slides that I’ve seen. I don’t know if this is really correct because I don’t have a compiler that actually supports this yet. In particular, I don’t know if there will really be a std::generator in the standard library.
std::generator<std::string> sendMessagesToOtherSolarSystemsAndGetResponses() { while (still_waiting) { ... // "return" one result via the generator. co_yield response; } // TODO: Do we need a co_return somewhere too? } int main() { auto messages = co_await sendMessagesToOtherSolarSystemsAndGetResponses(); // The range-based for loop waits for the next message each time. // TODO: When does it stop? for (auto response : messages) { std::cout << response << '\n'; } std::cout << "Not waiting any longer for more responses.\n"; return 0; }
So far that makes a generator look like a bit like a Go channel. But Go channels seem to be more versatile, as we will see.
Unidirectional channels
By default channels allow goroutines to both send or receive messages. This is rarely what you want inside a particular goroutine, so you can declare a channel type that is only for sending or only for receiving. You would typically create a normal channel and then the goroutine’s function would declare that it takes a unidirectional channel. A send-only channel is of type chan<-, and a receive only signal is of type <-chan. I do think that syntax is hard to love.
For instance, the function from our previous example really only needs a channel that it can send messages on, not receive them too, so we should change the type of the channel:
func sendMessagesToOtherSolarSystemsAndGetResponses(c chan<- string) { for i := 0; i < 10; i++ { ... c <- response } c.Close() }
The compiler can then help by preventing a message from being sent on the channel. When we pass the regular channel to the goroutine it will cast implicitly to the unidirectional channel. But we may not cast the unidirectional channel back to a regular channel.
I don’t know if there is some equivalent for this in the world of C++ coroutines and generators. Maybe it is generally unwise to compare C++ generators and Go channels.
Buffered Channels
So far we have created unbuffered channels. When we sent a message on the channel, the sending goroutine blocked until the message was received by the receiving goroutine. And when we tried to receive a message the receiving goroutine blocked until the sending goroutine had put a message in the channel. Therefore, unbuffered channels are also called synchronized channels.
But in our example, it would be better if the sending goroutine could keep adding messages to the channel without waiting for them to be received. We can use a buffered channel instead, specifying the size of the buffer:
c := make(chan string, 100) go sendMessagesToOtherSolarSystemsAndGetResponses(c)
The sending goroutine will then only block when the channel is full, and the receiving goroutine will only block when the channel is empty.
Using an Unbuffered Channel to Signal “done”
Unbuffered channels can be useful to signal that a goroutine has finished. This is mentioned in section 8.4.1 (Unbuffered Channels) of the book.
For instance, you will often want the main goroutine to wait for other goroutines to finish instead of stopping the whole program. By using an extra “done” channel, the second goroutine can signal to the main goroutine that is has finished, by sending an item on the “done” channel, and the main goroutine can block on that by attempting to receive from the “done” channel. For instance, here the goroutine does not return any responses so we need another way to signal that we have finished:
func sendMessagesToOtherSolarSystems(done chan struct{}) { for i := 0; i < 10; i++ { ... } done -< struct{}{} } done := make(chan struct{}) go sendMessagesToOtherSolarSystems(done) doOtherStuffInTheMeantime() <- done // This blocks
However, when the goroutine is already returning results via a channel, I believe it is simper to just wait for the channel to be closed, as we did in the previous example by using a range for over the channel.
The book recommends use of struct{} as the channel element, probably because it has no value. A bool seems more more concise, but you’d need a comment saying that the value (true or false) was meaningless. I guess you could also just do a range for over the channel, waiting for it to close, like when we used a channel to return responses, but without ever sending anything.
I feel that this “done” synchronization technique could have a more explicit syntax. I think that would make the code clearer and would mean that various code that does the same thing would have less uninteresting differences. Sync.WaitGroup (see below) seems to be what I want, but this raw “done” channel technique seems more popular for simple cases.
If we were using C++ coroutines, I’d guess we would do the same thing by waiting for the result from a std::future.
Waiting for Multiple Goroutines to Signal “done”
A variation of the “done” channel technique is useful when waiting for multiple goroutines to finish. If we know how many goroutines we have started, we can use a channel to wait for that many “done” messages. This is mentioned in section 8.5 (Looping in Parallel) of the book, though it doesn’t call it a “done” channel there. For instance:
func sendMessageToSolarSystem(coordinates Coordinates, done chan struct{}) { ... done -< struct{}{} } done := make(chan struct{} for coord range : solarSystems { go sendMessageToSolarSystem(coord, done) } doOtherStuffInTheMeantime() // Wait for as many done messages as goroutines that we started. // We could have counted them instead, and then counted down here. for range : solarSystems { <- done }
As section 8.5 (Looping in Parallel) of the book says, if you wanted to return something useful from each goroutine, even just an error, you would use that instead of a struct{}.
Again, I don’t know how we would do this with C++ coroutines.
sync.WaitGroup
As an alternative to the “done” channel technique when waiting for multiple goroutines to finish, Go provides the sync.WaitGroup concurrency-safe counter. It has Add() and Done() methods and a Wait() method that blocks until the count falls back to zero. This is mentioned at the end of section 8.5 (Looping in Parallel) in the book.
func sendMessageToSolarSystem(coordinates Coordinates, wg sync.WaitGroup) { ... wg.Done() // Doing it via defer would be safer. } var wg sync.WaitGroup for coord range : solarSystems { wg.Add(1) go sendMessageToSolarSystem(coord, done) } doOtherStuffInTheMeantime() wg.Wait()
Notice that there is no Add() method overload that uses a default of 1, because Go has no function overloads and no default function parameter values.
I don’t know how we’d do the same with C++ coroutines. Even clever and correct use of a std::condition_variable would not be helpful if C++ coroutines do not map directly to real kernel threads (see above).
Using a Channel as a Counting Semaphore
Section 8.8 (Example: Concurrent Directory Traversal) of the book suggests using a buffered channel of a specific size to allocate and release tokens, preventing too many goroutines from doing work at the same time, causing them to block until a token is available. In the example, this limits the use of a constrained resource, preventing the goroutines from opening too many files. For instance:
func sendMessageToSolarSystem(coordinates Coordinates, sema chan struct{}) { sema <- struct{}{} // Acquire token. defer func() { <- sema }() // Release it later. ... } // Don't try to send more than 10 messages at a time. sema := make(chan struct{}, 10) for coord range : solarSystems { go sendMessageToSolarSystem(coord, done) } doOtherStuffInTheMeantime() // This ignores the need to wait for completion.
I think I would prefer some syntax or API that let me explicitly specify a pool that allows only a specific number of coroutines to be active (or maybe even instantiated). Obviously that would only be useful in simple cases and it’s generally unpleasant to hard-code numbers anyway.
Select: Multiple results, from multiple channels, without blocking
Go has a select/case construct built in to the language just for channels. This is mentioned in section 8.7 (Multiplexing with Select) of the book.
It’s a bit like switch/case. It’s also a bit like the POSIX select() function in C, used to respond to activity on file descriptors, such as network connections. It can wait for activity on one of the specified channels. Or it can repeatedly poll if you supply a default case and put it in a loop.
c1 := make(chan string) go sendMessagesToExoplanetsViaRadio(c1) c2 := make(chan string) go sendMessagesToExoplanetsByBlockingTheSun(c2) for { select { case response := <- c1: fmt.Println("Response to or radio message: %s", response) case response := <- c2: fmt.Println("Response to or sun message: %s", response) default: if (too_late) { break } doOtherStuffInTheMeantime() // or sleep. } } fmt.Println("Not waiting any longer for more responses.")
I wonder how C++ could support a similar construct with coroutines and generators.
Canceling a Goroutine by Closing a Channel and Polling
The main goroutine might no longer need the result from a goroutine that it has started. Maybe another goroutine found the answer first, or maybe it was in response to a user request that has been canceled, or a connection that has been disconnected.
(Update: See the comment below, about Contexts, added in Go 1.7.)
Section 8.9 (Cancellation) of the book suggests signaling this by closing a channel, and suggests that the called goroutines should poll for this channel closure via select with a default case (see above). The book calls this channel “done” but I think that confuses this technique with technique that sends a done message over the done channel.
For instance:
// Poll to see if the channel has been closed. func canceled(canceler chan<- string) bool { select { case <-canceler: return true default: return false } } func sendMessagesToExoplanetsViaRadio(c chan<- string, canceler chan struct{}) { ... if canceled(canceller) { // Obviously not neat code: c <- "Not sent" } ... c <- response } c1 := make(chan string) canceler1 := make(chan struct{}) go sendMessagesToExoplanetsViaRadio(c1, canceler1) c2 := make(chan string) canceler2 := make(chan struct{}) go sendMessagesToExoplanetsByBlockingTheSun(c2, canceler2) for { select { case response := <- c1: // Cancel the other attempt because we don't need it. canceler2.Close(); case response := <- c2: // Cancel the other attempt because we don't need it. canceler1.Close(); } }
Again, I would like more explicit syntax or API for goroutine cancellation. For me, being able to implement things with channels doesn’t mean that they are the best API for expressing those things.
Communicating Instead of Sharing
Go programs tend to send messages via channels to change (indirectly) shared data and to let goroutines see the latest state of that data. “Do not communicate by sharing memory; instead, share memory by communicating.” is a popular phrase in the Go world.
This allows the use of data to be restricted to a single goroutine, often called a “monitor goroutine”. The locking (or just general lack of races if the Go channel is implemented with lock-free code) is then restricted to the channels, implicitly, with no explicit locking of the data being necessary. There are some examples of this in section 9.1 (Race Conditions) in the book.
It occurs to me that, unlike locking via a mutex, this does not guarantee that all goroutines see the latest state of any shared data. But it does guarantee an order, which is maybe all that usually matters.
Because channels are concurrency-safe, you can even send them as elements of a channel. Section 8.10 (Example: Chat Server) does this so it can use “client” channels as identifications of client goroutines (as well as sending strings over the client channels), keeping track of them via entering and leaving channels and a map of the client channels.
Concurrency-safe containers
We’ve already seen that Go channels are “concurrency-safe”, so you can share them between goroutines without additional locking. So are slices and maps.
Update: No, slices and maps are not concurrency safe. I must have misunderstood something that I read somewhere. This makes the rest of this sub-section irrelevant, though hopefully correct.
Interestingly, the Java standard library moved away from making container types thread safe, because (as I understand it) the developer would generally use them as part of a larger custom data structure that would need its own locking (synchronization) anyway. Java now provides some separate containers explicitly for use with concurrency.
Likewise, in the C++ standard library, types only tend to have locking (for instance std::shared_ptr’s “partial internal synchronization“) when it wouldn’t be possible for the application code to do the locking itself (external synchronization). Presumably, Go programs then tend to have more locking that necessary, and presumably this is Go again choosing convenience and safety over raw performance.
However, I would really like to see a concurrency-safe queue in C++, similar to a Go channel. A lock-free concurrent queue seems like exactly the kind of difficult-to-implement, but easy to use, and broadly useful, thing that belongs in the standard library.
No thread-local storage
Because Goroutines don’t map directly to real threads, Go doesn’t try to provide “thread-local storage”. C++ has this, via its thread_local keyword, but I believe this is not useful, or wise, with C++ coroutines either.
Mutexes
Goroutines are a suitable way to co-operatively schedule work, and communication over channels can avoid the need for explicit locking, but you might still need to explicitly prevent concurrent access of shared data. Mutexes do this in both Go and C++, but feel free to skip to the summary if you only cared about Goroutines and channels.
Go’s sync.Mutex is like std::mutex
We lock/unlock Go’s Mutex, like so, though you can also do it manually instead of using defer.
m sync.Mutex func something() { m.Lock() defer m.Unlock() // do something to the data. }
which corresponds to this in C++ with std::mutex:
std::mutex m; void something() { std::lock_guard<std::mutex> lock(our_mutex); // do something to the data.h }
I’ve always wished there was some shorter syntax for that lock_guard in C++.
Note that you can use std::unique_lock, instead of std::lock_guard, if yo need to manually lock and unlock.
Go’s sync.RWMutex is like std::shared_mutex with std::shared_lock
A Read/Write mutex (or “shared exclusive lock”) allows multiple threads to read from shared data, while ensuring that they each see the latest state of that shared data, but only allows one thread at a time to write to the shared data.
We lock/unlock Go’s Mutex, like so, though you can also do it manually instead of using defer.
m sync.RWMutex func somethingThatReads() { m.RLock() defer m.RUnlock() // get something from the data. } func somethingThatWrites() { m.Lock() defer m.Unlock() // do something to the data }
which corresponds to this in C++ with std::shared_mutex:
std::shared_mutex m; void somethingThatReads() { std::shared_lock<std::mutex> lock(our_mutex); // get something from the data } void somethingThatWrites() { std::lock_guard<std::mutex> lock(our_mutex); // do something to the data }
I do find the Go names more obvious here. They seem to be aimed more at how people will actually use these locks in most situations.
No recursive/re-entrant mutex
Go mutexes, like C++’s s std::mutex and std::shared_mutex, are not re-entrant, so if a thread tries to lock a mutex that it has already locked, there will be a deadlock. C++ does provide a re-entrant mutex via std::recursive_mutex, but actually using it is generally considered rather disgusting and a sign that the code should be restructured to avoid it.
As the Go book wisely says. “The purpose of a mutex is to ensure that certain invariants of the shared variables are maintained at critical points during program execution. One of the invariants is “no goroutine is accessing the shared variables,†but there may be additional invariants specific to the data structures that the mutex guards. When a goroutine acquires a mutex lock, it may assume that the invariants hold. While it holds the lock, it may update the shared variables so that the invariants are temporarily violated. However, when it releases the lock, it must guarantee that order has been restored and the invariants hold once again. Although a re-entrant mutex would ensure that no other goroutines are accessing the shared variables, it cannot protect the additional invariants of those variables.”
Summary
Go’s concurrency features are another example of its choosing safety and simplicity over potential raw performance, in an area where standard C++ does not yet offer such simplicity even as an option.
Both support standard mutex locking, in much the same way. But otherwise their strengths go in opposite directions:
C++ lets you write lock-free concurrent code so the compiler, with the help of some hardware features, can arrange for the compiled code to never attempt simultaneous changing of your shared data, while ensuring that different threads see changes when necessary. But that is notoriously difficult to get right. Nevertheless it needs to be available so we can benefit from the work of people who know how to use it.
On the other hand, Go lets you write concurrency code that is easier to get right than when just using standard locks There is probably some small performance cost, but it’s likely to actually work and you’ll probably understand how it works. After all, when concurrency code is wrong (C++ makes it easier to be wrong), performance can suffer as well as safety.
I do have some hope that things will get better for C++ when coroutines arrive and are widely understood, but it looks like they alone might not be enough to match Go’s goroutines ad channels.
>So I guess you still wouldn’t want, for instance, a goroutine per network connection, if you are serving many network clients.
It’s pretty common to use a goroutine per network connection. AFAIK, Go won’t create a separate hardware thread for each network connection. I believe it creates a single network thread to do the polling and wakes the proper goroutine when there’s something for it to do.
What Matthew said. You can in fact have hundreds/thousands of goroutines waiting for network I/O under a single OS thread efficiently because Go’s runtime uses an “event pool” to wait for network events and wake up specific goroutines when there is work for them to do. This is actually the single greatest reason for using Go instead of C++/python/etc for networked daemons (and IMO why Go has been doing so well in this niche). You get the benefits of true parallelism (which python lacks, even with asyncio) and concurrent execution (because the runtime does the heavy lifting of scheduling continuations for you on I/O events) while feeling like an imperative language (instead of the callback mess that is js).
Also, for generators in Go, you can do something like
“`
func sendMessagesToOtherSolarSystemsAndGetResponses() chan int {
c := make(chan string)
go func() {
for i := 0; i < 10; i++ {
…
c <- response
}
c.Close()
}()
return c
}
go doOtherStuffInTheMeantime()
for response := range sendMessagesToOtherSolarSystemsAndGetResponses() {
fmt.Println(response)
}
fmt.Println("Not waiting any longer for more responses.")
“`
Thanks for that explanation. I guess network connections are a special case, to which that sentence from the book is not really relevant.
Yes, that “generator” would be one way to avoid declaring the channel type before calling a function. Thanks.
@Dan JS since ES 2015 has futures/Promises as first citizen in language. Earlier it had `defered` objects thanks to userspace.
So “(instead of the callback mess that is js)”, don’t exist anymore.
Example: you could to wait for all concurrent functions/routines finish. That way you do not need to count “done” callbacks.
I think you might be wrong stating that channels are implemented in lock-free manner. They do not use sync.Mutex indeed, but they do use locks: https://github.com/golang/go/blob/master/src/runtime/chan.go#L169 (see: https://github.com/golang/go/blob/master/src/runtime/lock_futex.go and https://github.com/golang/go/blob/master/src/runtime/lock_sema.go).
I might be misunderstanding you, though :)
Thanks. But I didn’t state that channels are implemented with lock-free code, as far as I can tell. I did suggest that it might be possible, though I have no knowledge whatsoever of the implementation: “The locking (or just general lack of races if the Go channel is implemented with lock-free code) is then restricted to the channels”
Fair enough, I was not sure what you meant.
Your channel example has a typo. You create and pass a channel of strings to a function that takes a chan of int. Then you don’t show actual sending the value to the channel. You still return a value from the function.
func doSomethingThatTakesALongTime(c chan int) int {
…
return 5
}
c := make(chan string)
go doSomethingThatTakesALongTime(c)
doOtherStuffInTheMeantime()
answer := <- c
Thank you. That did make the example particularly useless. I’ve corrected it.
Your information on cancellation is a little outdated since it came just from that book. As of Go 1.7, Context has been made a first class concept in the stdlib: https://golang.org/pkg/context/
It allows hierarchies of pipelines to be built with cancellations and timeouts. It is the more formal cancellation api to which you were referring.
Thank you. That is really helpful. I have added a brief mention of that to the text.
Go’s container types are *not* concurrency-safe. You have to lock them for reading and writing.
Go 1.9 has added a new container to the standard library that will do locking for you, but it doesn’t behave like it’s built into the language.
Thanks very much. I was sure I had read that in the book, but apparently I did not. I have updated the text to mention that this was just wrong. Thanks again.
“Presumably there is some very clever scheduling code in Go’s runtime and in the C++ coroutines implementation.”
I don’t think so. Basically co-routines are green threads, both in C++ and Go. The difference is with blocking I/O: The Go runtime in this case automatically seems to spawn a real thread, to not block the process. Whereas in C++ that will be left up to the developer. I.e. With C++ co-routines, you’ll not get non-blocking I/O and multi-Core out-of-the-box, but you need to combine them with real threads (std::thread or std::asynch, etc.). So they are rather building blocks to ease working with concurrency, but not a replacement for real multi-threading, like go-routines.
Your first std::async() example doesn’t quite do what you say it does. The following statement is effectively synchronous in C++:
std::async(doSomethingThatTakesALongTime);
It returns a std::future that will immediately be destroyed. And the destructor for futures returned by std::async waits for completion, so won’t complete until doSomethingThatTakesALongTime() completes in the other thread.
Also, the C++ “co_await” doesn’t seem like a an equivalent of Go’s “go”: rather than spawning a concurrent task, doesn’t it block the current coroutine and wait for some other task to complete?
As for using one goroutine per client in a network server, that is generally fine in pure Go code. The Read() and Write() methods on the connections managed by the “net” package do not map directly to the equivalent system calls. Instead, they will cause your goroutine to block while a single OS thread polls all the fds.
Lastly, I think it is a bit confusing to talk about unidirectional channels, since all channels only provide a single direction of communication. It’s just that Go uses a single variable to represent both end points of the channel. The restrictions cause the language to block you from using one of the endpoints. A channel is never bidirectional as sockets are, so if you need bidirectional communication you really need two channels.
> It returns a std::future that will immediately be destroyed. And the destructor for futures returned by std::async waits
That’s a very good point. Corrected. Thanks.
> Also, the C++ “co_await†doesn’t seem like a an equivalent of Go’s “goâ€: rather than spawning a concurrent task, doesn’t it block the current coroutine
Possibly. I don’t know. I assumed that the blocking happens when we later try to get a result, not immediately. I am really eager to see how this stuff will really work and I’m really disappointed at the lack of useful documentation so far.
> I think it is a bit confusing to talk about unidirectional channels
I agree, but I decided to use the terminology that is in the book and not criticize it. Maybe people don’t use that terminology much now.
Thanks again.
> Possibly. I don’t know. I assumed that the blocking happens when we later try to get a result, not immediately.
Looking at your first C++ coroutines example:
int answer = co_await doSomethingThatTakesALongTime();
If co_await is returning a value stored in an integer variable, how could the wait happen later? From what I can tell, it is the equivalent of:
int answer = doSomethingThatTakesALongTime().get();
… except that rather than blocking the OS thread on the future, it lets other coroutines execute while the future isn’t ready.
Go will let you implement lock-free data structures at the low level, same as C++, if you must. The sync/atomic package provides the load/store-with-memory-barrier and CAS operations you need in order to do this. For pointer types, however, it does take a lot of casting and use of the unsafe.Pointer type. So it isn’t pretty to read (nor to write).