While Go in theory is the same as a 1990s-style-threading model, and serious Go programmers had best not forget that in the final analysis they aren't actually protected from multithreading problems simply "because Go", having a model where structures are owned by particular threads, and things are shared via messages rather than direct threads, thus making it the thread's responsibility to be ready for incoming messages, makes it vastly, vastly easier to program in in practice.
Yes, this absolutely could have been adapted back then, too. This once again goes back to the fact that a language is often more than just the language itself, but also includes the standard idioms and the standard and common libraries. You could have written your code in 1990s in the Go way in C++, but you would have had to bring up basically an entirely new library stack and written a lot of wrapping code around OS functionality.
Despite the fact that Go programming is in practice much simpler than the threading being criticized in that paper, I agree strongly with the idea that Go would be enhanced by the ability to erect much stronger boundaries around goroutines to ensure that they neither share nor reach into anything they shouldn't. Still, there are ones by convention, which is way better than nothing.
You should also consider Rust-style "synchronization witchcraft" as a very interesting alternative "isolated actors" and "locks everywhere" that strikes out in a new direction entirely. In 50 years what we currently consider all of our best sync options may in hindsight have proved to been simply a false dichotomy (albeit one with three or four choices rather than just 2, but, the same principle holds).
I kind of disagree, the Go mantra «don't communicate by sharing memory, share memory by communicating» is merely a marketing pitch, especially due to the clumsy nature of Go's channel[1].
Unfortunately, the number of Go developers being bitten by data races in Go is too high … The main danger here is the illusion of safety that the Go community is spreading (and your comment is an example of that): all the Go devs I know (including me) never really worked on multi-threaded C or C++ code before, and if you come from a nodejs or Python background like most do, you aren't really prepared to understand the challenges of multi-threaded programs. Sometimes the best way to structure your program is just to have a shared `map` of items you need to process, but if you forgot to wrap it in a mutex, you have a data-race. And if you don't even know what a data-race is, you face strange and unpredictable bugs (true story).
[1] : http://www.jtolds.com/writing/2016/03/go-channels-are-bad-an...
You definitely do have some responsibilities learning how to write truly multithreaded code after coming out of a single-threaded environment. However, having used quite a few environments, I don't know of any where that isn't true. You've fundamentally stepped up a level of complexity. In Haskell, you don't have to change too much about your style, excepting that you first had to write your code in Haskell, which most people consider quite a leap. In Erlang, you are forced to deal with process boundaries and there isn't really an equivalent of "just share a map"; even the obvious "just share a map" option (ets) is simply a prepared API around a process boundary. Rust's pretty cool, but you must write in accordance with its type system, and that's going to fundamentally change how you write programs if it's your foray into that sort of world, which is cool and good, but far more disruptive than learning how to write some Go.
If Go makes a particular error here, it is that it doesn't stick the problems in your face and makes it a bit too easy. I'm actually on the side of that being a problem (if you think I'm all "rah rah" Go, go back and read the grandparent post with an eye for counterexamples, there's at least three), but I think most people coming from Python or Node would at least initially believe they prefer this.
The reason I am a bit rah-rah on Go (just not unreservedly) is that while I still if I had my way would tweak some important aspects about it is that it's the first time it has brought that style of thinking (and libraries!) to the Algol-style languages. On the one hand, as a language dilettante I'm saddened that we lose some goodness from the non-Algol heritages, but on the other hand, it means I get to use it for my job without cringing. Haskell is... Haskell. Erlang's a neat language but it conflated a lot of stuff and forces a lot of adaptations on to you that I consider unnecessary. (I respect it and its developers deeply, but as is often the case the first effort in a certain space often makes errors that later generations will consider basic.) The C/C++/Java/C# space all pretty much went with 1990s-style threading. It's probably still the easiest entry into that space you can have, so I have a hard time laying too much blame at the language's feet rather than the problem domain's feet. (The channel criticisms are mostly legit, but at the same time, Go channels are probably better primitives than 80% of programmers have ever had access to. To see the complete picture one most manage to hold all of these aspects in one's head at once.)
But yeah, people need to stop confusing actor model with shared-memory multithreading. Threads/green-thread/goroutines are all the same and the criticism applies to all of them. They share nothing with actor model (pun intended).
Goroutines are 'soft threads'. The underlying implementation typically defaults to number of CPU cores as max possible REAL thread count. The real threads will check the message queues for possible wakeups when the previous soft thread blocks or terminates.
The fork-join model doesn't have this problem because you can only join a determinate number of tasks, and you have to join them in a determinate order. You can't have anything arriving at any other order than you specified. Scheduling issues cannot change how a fork-join program runs.
In the actor model you also have a really complicated state situation. You've got n actors, all each in a possibly infinite number of states. Because of the non-determinism described above the states your actors will get into in each run of the program can be different. That's another part of what makes multi-threading so hard.
The fork-join model doesn't have this problem because tasks don't have any state. They just have input and output. There's no state, only commutation.
The actor model is a big uncontrolled graph of state in my opinion. Fork-join is DAG of pure functional computation done in parallel.
In this model I think deadlocks is possible, like in CSP. I wonder if CSP is closer to this model, right?
Fork-join looks like, where we blur an image with two-way parallelism:
a, b = divide image into halves
fork {
a' = blur a
b' = blur b
}
result = combine halves a' b'
So all the tasks in the fork { } block run in parallel. The task that created those tasks stops and waits for them to all finish and to have all their results before it continues.And there's no way the order of the tasks finishing can change the result because you always have to wait for all your tasks to finish.
See how there's no mutable state? You can only start a task, and accept its result. You can't see or change anything in between that.
You can get a deadlock if one task crashes. Some people really care about that, some people see it as an implementation detail and wouldn't really define it as a 'deadlock' in the same way.
Systems which have fork-join built-in would either help you to follow those rules, or force you to.
fork -> send -> task1(input) & task2(input) -> joiner
And you must always(?) have a join? so I can't have a zombie task elsewhere?I probably like how this sound! Exist a more detailed material to study on this?
One of the more popular actor model languages (Erlang) also generally avoids mutable state (it's available in the form of the process dictionary, but recognized best practice is to avoid using it.)
I mean an Erlang actor is a state machine isn't it? It accepts messages, and each messages causes the actor to either return some data based on its state, or to enter a new state.
State machine - so it's holding state isn't it? And it's mutable because you can cause it to enter a new state by sending messages.
And you have lots of these mutable state machines in your Erlang process - lots of mutable state.
See what I mean? I know they don't have shared memory, but each process is effectively a little piece of global shared mutable state. And that's bad!
In other parallelism models like fork-join, there is no mutable state. Tasks get input parameters, and produce a result. They don't have any state in any meaningful sense because you can't observe their state externally and you can't ask them to change state externally.
Yes, but only if there can be structural sharing between threads.
https://blog.stephencleary.com/2013/11/there-is-no-thread.ht...
There are times when you can offload an asynchronous task to non-CPU hardware, in which case the blocking thread can disappear. The thread reappears when the hardware asynchronously informs the CPU of task completion. (This idea is what Node.JS is entirely reliant upon.)
Consider if we didn't have timing hardware to handle this task for us. We could implement a timer with a spin-loop thread which checks the clock and fires off events.
How's that for an honest response lol.