What's really hard is to break down a problem into parallelizable chunks, figure out as much independent work as possible to reduce the touchpoints, and coordinate all those tasks such that they keep the CPU as busy as possible and as a whole finish as early as possible.
Beyond this "parallel breakdown design", it's the little touchpoints with shared data structures and synchronization that creates the difficulty of implementation, and I haven't seen any language or system that does magic there.
Common mistake number 1. Goroutines are running in parallel AND concurrently - they are coroutines (common mistake number 2 is to think they are only coroutines). I suggest not to underestimate that, it's the big deal. Additionnaly, it's important to note that goroutines yields on sleeps (any kind of sleeps / waits, like disk reads, network requests, channel writes/reads, etc), and while it may sound like a detail, it's a wonder of cpu control. Due to that, there is also a rare elegance to the way Go solve sharing data via channels.
What's really hard is to break down a problem into parallelizable chunks, figure out as much independent work as possible to reduce the touchpoints, and coordinate all those tasks such that they keep the CPU as busy as possible and as a whole finish as early as possible.
That's exactly what Go is a wonder for due to the combination of parallelism, concurrency, yield-on-sleep and channels.
I'm unsure if the intent of not including a map function was due to this, however with a for loop such as
for x := range ch {
slice = append(slice, x)
}
, it is immediately obvious there are allocations happening in the background.The fact that map access, and slice access is not thread safe means there is no trickery going on in the background. The fact that it is not threadsafe means I don't need to worry about a lock if I only write to a map when it's created. Sure the compiler could take care of this - they have the race detector after all, but the compile speed is one of the design goals of go. I really like being able to compile in less than 1 second.
`sync.Map` however calls a function which implies there is more going on in the background.
If you follow the go mantra of share memory by communicate, don't communicate by sharing memory - handling data becomes a whole lot easier. It does allow you to share memory in case you do need the extra speed.
Like most things, all designs are a matter of trade offs. Sacrifice one thing for another. There are other languages that provide the functionality you desire - but I understand the frustration when one thing is 90% what you want.
Go obviously has room for improvement, and perhaps a native threadsafe map is one of those areas.
That it can't do something effectively does not mean that it shouldn't or didn't mean to. (And users can't really fix it, because hey, no generics. Sigh.)
I'm pretty sure I disagree with that largely because Go is low-level enough so that the GC is easily handled.
And users can't really fix it, because hey, no generics. Sigh.
Users can fix it by writing languages on top of Go (particularly dynamic ones).
Not sure how rare, .NET does that as well. Below is your sample translated to C#. It requires C# 7.1 because async main, but the rest of the stuff is available for many years, since 2010.
static async Task routine()
{
await Task.Delay(TimeSpan.FromSeconds(1));
}
static Task Main()
{
return Task.WhenAll(Enumerable.Range(0, 1000).Select(i => routine()));
}I don’t have hands-on experience with golang but based on what I know about it yes, they are.
> there seems to be a call to yield but its unclear what it really does
You mean “await”?
It waits for the completion of whatever is on the right side of “await”. If the result is already available, it just continues. If the result is not yet available (e.g. Task.Delay creates a task that will complete in some moment in the future), the control goes away from the async method to the scheduler. The scheduler can then run some other task on the same OS thread. When the result of that operation becomes available (in the sample code, when 1 second delay passes), the scheduler resumes execution of that async method, on the statement after the await.
> if they yield on any sleeps
No, not any sleep. You can call Thread.Sleep() which will put the whole OS thread to sleep. It’s up to programmers to avoid calling blocking APIs from their async methods, i.e. use await Task.Delay() instead of Thread.Sleep(), await stream.ReadAsync() instead of stream.Read(), and so on.
If Go's m:n is blowing your mind, you should check out Erlang. Spawning a million "processes" is not a big deal.
Additionally, combining them with the power of channels makes quick work of many tasks. Channels of course can be implemented in C++ too, but having the compiler take care of it for you with additional tools such as the race detector is very handy.
For a large set of problems, they are very nice to work with.
Then there is the ongoing work to add async/await patterns into C++20.
Notice how you can always have that answer when it comes to programming languages: you can do the same in. The point is that, the way it's made in go, is awfully handy.
go func () { }
and Task.Run(() => { })
With the benefit that on the latter example, the runtime allows me to customise how scheduling is done.These aspects are key to goroutines.
It seems like both languages are equally capable here, with C++ having more power and foot guns when required as usual.
C++ with PPL on Windows, would be
task<T> handle = create_task([] { /* ... */ });
And with standard C++ future<T> handle = std::async(std::launch::async, [] { /* ... */ });
In both cases, with C++20 it will be possible to co_await handle, which you can already play with on clang and VC++.This is probably exactly the same behavior as goroutines.
Goroutines and .net tasks allow a nice mix of CPU bound and IO bound code. While a goroutine/task is waiting for IO or something else to complete (timer in this example), the runtime will immediately use the hardware thread for some other task, without OS involved.
Once you execute truly on multiple CPU cores (by increasing GOMAXPROCS), you'll be having the same kind of race conditions in Go as in any other imperative language (inb4 Rust Evangelism Strike Force saying "except Rust").
Wrong. GOMAXPROCS defaults to the number of logical CPUs, IIRC since version 1.5. For example, I have four cores with 2 threads each, so goroutines will be executed on up to 8 threads unless I set GOMAXPROCS to something else or the application explicitly changes it using the runtime package.
And sure, you'll really have the same problems, but IMO channels and goroutines minimize the friction of implementing thread safe programs using CSP. GP seems a bit optimistic, agreed, but I think that there is at least some substance to the idea that go makes it easier to correctly utilize multiple cores.
GOMAXPROCS defaults to number of cores.
Prove it by replacing Sleep in your example with some number crunching, and show how it scales with the number of cores in your CPU.
Running the following code.
https://play.golang.org/p/k_rRxNAyb0i
I can assure you that go runs across all processors by default.
https://docs.google.com/document/d/1At2Ls5_fhJQ59kDK2DFVhFu3...
In case you dont know, Golang goroutines are a marvel of parallelism. They are coroutines which are dispatched on a few OS threads. So you can use 100% of a multi-cores CPU and yet, spawn, say, 10K of those light threads without worrying about context switches PLUS have them all run concurrently. I've found that golang is one of those rare language, like Lisp, that actually change the way you think about programming. Makes you feel really more powerful.
If you dont know that language, I suggest to run the following and watch your cpu activity and memory (or any metric):