Why Threads are a Bad Idea (for most purposes) (1995) [pdf]
web.stanford.edu
web.stanford.edu
Why they're equivalent (duals): https://blog.acolyer.org/2014/12/08/on-the-duality-of-operat...
Why threads are a bad idea: https://blog.acolyer.org/2014/12/09/why-threads-are-a-bad-id...
Why events are a bad idea: https://blog.acolyer.org/2014/12/10/why-events-are-a-bad-ide...
Unifying events and threads (in Haskell): https://blog.acolyer.org/2014/12/11/a-language-based-approac...
Unifying events and treads (in Scala): https://blog.acolyer.org/2014/12/12/scala-actors-unifying-th...
They are easier to program than events, they still have problems like its easy to run out of a very limited stack and you have to yield your cooperative thread often to other tasks, but they are easier to maintain than a state machine (that is often done as a very big switch statement).
It also means my application can remain very simple. Ie. I don't need to think about things like Celery or Redis or handling locking meticulously.
yes, you need runtime support for this feature. If it is impossible to change the runtime than it can't be done for the language; now the runtime may have problem to work with a very limited stack for example, or the runtime is big and difficult to change (like the JVM), or it has long library functions that cannot yield to other co threads.
However C/C++ or other languages that do not sit on top of a big runtime dont have that problem.
In the meantime I have the feeling that both threaded and single-threaded event-driven programming programming models work well, and that the main thing to avoid is mixing both.
E.g. I found that using callback based or push-style APIs (like Reactive Extensions) in multithreaded environments is not a great idea, since it often won't be obvious on which thread a callback will be running and which kind of synchronization is needed. Not using callbacks and instead waiting/blocking for results works better. In a pure singlethreaded environment it's generally easy to know what to expect from a callback or promise - it will be fulfilled from the same thread and the continuation will also run from the same thread. In a multithreaded environment it can get hard - e.g. a promise continuation (like Task.ContinueWith() in C#) could run in any thread depending on the setup. And blocking on promises (like with Task.Result) may be valid in some situations (where the Promise is fulfilled from another thread) but not in others (where the Promise is fulfilled from the same thread). Using only synchronous primitives in multithreaded environments (like it's common in Go) avoids these pitfalls and questions.
Sometimes having a singlethreaded eventloop even in multithreaded environment however makes sense, like e.g. for GUIs (and other applications which have lots of shared state). In that case it should be assured that all objects which are living on the eventloop should just be accessed from other objects on the same thread - and do not involved directly in function calls from other threads. If that's required posting events or function calls to the eventloop thread is a much saner approach then trying to add low-level multithreading synchronization primitives everywhere.
Learn concurrent programming techniques — or perish. Threads and sync primitives are low-level, but important, and you have to understand them to figure out what compromises and biases were taken in higher-level models.
And, frankly, it isn't that bad (debugging existing code is bad, but playing with monitors and semaphores and critical sections is easy, until code is small and isolated).
Successful, popular concurrent platforms like Erlang/OTP, Nginx, and node.js, eschew threads in favor a single-threaded, async/non-blocking code model. These platforms simplify application-level code by avoiding the issues of thread synchronization and contention in a shared memory space, and instead provide/require isolated processes to exploit CPU-level parallelism.
The threading "isn't that bad" viewpoint generally comes from a limited understanding of the things that can go wrong - for instance, add "memory barriers" to the list of things to understand.
There's a good explanation of the problems with a common Java idiom "double-checked locking" at https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedL...
Of course, when things get complicated with lots of interactions between processes, or significant amounts of computation in a single-threaded process, some platforms require the user to manually yield, etc., basically trying to re-create preemptive threading provided by the OS.
Erlang, the language, is threading model agnostic, but the main implementation has been, by default, M:N threaded on systems with multiple logical processors since, AFAICT, the R12B release in 2008. So characterizing it as single threaded is inaccurate.
The BEAM VM hosts Erlang processes in a multi-threaded single process. This is an implementation detail/performance optimization.
The Erlang processes don't care if they are hosted in a single OS process or multiple processes or even multiple host machines. Erlang-the-platform simplifies application code by providing a single-threaded, share-nothing, async/non-blocking code model. (Edit: Yes, dfox, it's probably more accurate to say, Erlang provides a synchronous, blocking code model, since that's what the Erlang programmer sees)
Erlang processes share memory, and some message sends (local sends of binaries > 64 bytes in size) are implemented by sharing references to shared, reference-counted, objects. This is safe because (not considering the process dictionary, which isn't shared) Erlang processes have no mutable state, and all that is shared is immutable values, so if the implementation wanted to, all sends could be references to shared memory (for small objects, the indirection and reference counting overhead isn't worth the memory savings, so its not done.)
It's not possible to write a high-performance program without a good understanding of the costs of primitives. If something might vary in cost by several orders of magnitude, it's quite hard to design for.
Network transparency has never been a good fundamental for well-performing application architecture. Networks are inherently far less reliable than shared memory, and have massively more latency, particularly on the tail end of the distribution.
Nginx and Erlang most certainly use multiple threads by default. They both use thread pools. I'm not sure about Erlang but Nginx has a single master thread that does async select on the main socket. Once it receives a connection it hands off to worker threads. This is a traditional multithreading setup with control/worker threads.
Node doesn't support threads because JavaScript doesn't. Performance would certainly be better for many workloads if it followed a traditional multithreading approach like Nginx. Async != Single threaded. They're not related concepts. You get the best performance using both non-blocking calls and multithreading , an approach used by Nginx and Netty. They both use nonblocking IO on the main thread and thread pools.
Node requires you to manually yield by calling Response.end() because it doesn't support multithreading. This basically duplicates OS level pre-emption manually and is my single biggest criticism of node. The worker/master thread model allows the runtime to manage worker resources or just defer to the OS in a flexible way.
Processes are isolated and do not share memory. Threads within a process share memory. For example, see https://msdn.microsoft.com/en-us/library/windows/desktop/ms6...
Nginx is single-threaded, multiple process: "Each worker process is single-threaded and runs independently" https://www.nginx.com/blog/inside-nginx-how-we-designed-for-...
Erlang code is single threaded. Each Erlang "process" is an isolated, share-nothing single sequence of instructions, and you don't use semaphores, locks, or critical sections in writing Erlang code. The BEAM VM is implemented as a multithreaded process, but this is not exposed to the Erlang code, and Erlang code can be transparently distributed across multiple instances of the BEAM VM (processes) even on separate hosts.
You're correct on node.js, and "basically trying to re-create preemptive threading provided by the OS."
The "nothing shared" model used in Linux forking actually shares the underlying memory until the processes diverge (copy on write). The difference between this and threads is that threads expose the shared memory by default.
IMO the difference between threads and processes is somewhat academic.
I don't know much about BEAM
Shared memory vs. isolated memory is a huge difference. You use completely different sync/protection/IPC approaches with each, so programs look very different. And the copy-on-write optimization of logically isolated memory doesn't effect that. The beauty of it is that it speeds up forking and saves memory without changing your IPC-based programming model.
At a linux-implementation level the difference is almost irrelevant. At a code level the difference between your variables being shared by default versus "thread"-local by default is huge.
The same is true for PHP.
No wonder Facebook developed HHVM for PHP and also uses Erlang with WhatsApp and other parts.
Nginx has a master process and worker processes. Master process doesn't do anything, but manage worker processes in case they die or need to be restarted on configuration change. A worker process runs a single event loop that accepts connections. It doesn't hand off them anywhere, but just schedules connection handlers to be run in the same event loop. Worker processes don't share memory (almost, there are some sysv shared memory segments for a few things, but this is irrelevant). Thread pools in nginx were added only relatively recently to do blocking file I/O on linux, which is kind of slow otherwise in some scenarios.
Except that Node (by way of libuv) uses thread pools internally for things that it deems to be CPU-intensive enough to need it, like crypto.
Multi-threaded programs have to be concerned with low-level details like memory ordering and barriers only when they do synchronization without using OS-provided utilities or when they implement synchronization wrong. pthread_mutex_lock() is both memory and optimization barrier and that is probably all one has to know for vast majority of MT code.
Yes, but there are lots of ways to implement synchronization wrong because you don't understand memory barriers.
For instance, Doug Lea's article linked above includes this very simple Java code, which breaks because it wasn't aware of memory barriers:
// Broken multithreaded version
// "Double-Checked Locking" idiom
class Foo {
private Helper helper = null;
public Helper getHelper() {
if (helper == null)
synchronized(this) {
if (helper == null)
helper = new Helper();
}
return helper;
}
// other functions and members...
}
[dfox: can't reply, guess we're nesting too deeply.
Per the above, the simple rule has to be modified to "do not touch OR READ shared state outside of relevant critical section". Easy to make errors...]To some extent the cited problem is unique to Java, because the whole monitor concept just creates confusion about what synchronized really means (and also creates significant overhead by making literally everything usable as mutex). Pretty common misconception is that synchronized is akin to some kind of read-write lock (in which case the code above would work as expected).
Sometimes, threads are the preferable solution.
Ugh. As someone suffering with a node.js project now I'm continually chafing under this limitation. It's great if you're making a simple CRUD website, but you'll be beating your head against the wall if you have a problem domain with centralized resource sharing that prevents you from spamming processes willy-nilly.
And node.js code is ugly.
>The threading "isn't that bad" viewpoint generally comes from a limited understanding of the things that can go wrong - for instance, add "memory barriers" to the list of things to understand.
Actually, threading isn't very hard to do right. It's more a question of choosing a reasonable architecture. Sure, you can get yourself into a lot of trouble trying to interleave threads in an attempt to use multiple processors to speed up a single operation. Don't do that.
>There's a good explanation of the problems with a common Java idiom "double-checked locking" at https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedL....
Yes, and if you read to the end of the article you'll see the problem it's addressing was fixed 12 years ago in Java 5.
As someone who's implemented reliable, massively parallel threaded systems in Java, when I hear people say threading is too complicated to be used safely I feel like a blacksmith who's just been told horseshoes are impossible to make even though he's been making them his whole career.
I've also implemented reliable, parallel, `threaded systems in Java. But I've also found defects in code that I thought had been thoroughly exercised, especially when it has moved to a new environment or new level of parallelism.
There's often "one more thing" that can cause problems, and generally it can be resolved by one more concept or workaround (like adding "volatile" in Java 5).
And I'd suggest Erlang/Go, which are designed for CSP-style concurrency, for cleaner code than node.js. JavaScript-based platforms often feel like, "Wow, you can even make JavaScript do X" rather than, "X is really natural in JavaScript."
It still proves the OP's point that this stuff can be come complicated.
What I worry about are things that can't be easily caught. I trust myself to write embarrassingly parallel code, and I've written some code that has real risks of deadlock. But shared data structures that have complex locking schemes still scare me.
All these platforms have some problems. E.g. Nginx is PITA if your processes need to communicate with each other. Erlang/OTP has this nasty mailbox problem (O(N^2) behavior of the selective receive). The cooperative multitasking is a piece of Victorian-era technology that is so painfully bad for various reasons, but people still tend to believe that is solves something.
On the other hand, modern operating systems are awesome. The thread schedules are awesome. Thread schedulers can dynamically adjust thread priorities depending on the load and solve some nasty synchronization problems (like priority inversion or starvation) for you. It's not 1995 and OS schedulers can switch threads in O(1) and most server apps can use a thread per connection approach without any problems. The only thing you need to get right is a synchronization, but there are a lot of tools that can help (like Thread Sanitizer). It's much harder to get the synchronization right with cooperative multitasking (good luck finding that priority inversion on implicit lock caused by dumb channel use pattern) than with normal threads.
At the machine level, these are all done with interrupts, shared memory, CAS & LL/SC instructions, and memory barriers, which actually tend to map more closely to the event model.
In the middle, there's a whole family of concurrent data structures - semaphores, producer/consumer queues, mutexes, transactions, etc. - which really are worth learning about.
Don't get me wrong; I love a nice, concurrent and efficient system as much as the next guy. I've been in various kernels and embedded systems doing the whole spinlock/reader-writerlock/mutex scene, and it's fun as hell. But in the end, if I can spend a few more cycles and not have to worry about race conditions and debug hairy crashes all the time then I'm far more productive, and ultimate the customer is happier.
I've learnt and used low async primitives for years, but for many (most?) applications, as long as you know what you are doing (which i guess is your point), using them directly isn't a very good idea anymore.
You can't even do that on any modern mobile device without thermal limiting kicking in quickly(although GPU/SoC vendors love to turn that off in standard benchmarks).
In the context of that time, "only when you're trying to achieve CPU concurrency" was meant as "only if you're doing something exotic" (though it wasn't as exotic as Osterhout thought - I'd already been working on MP machines for years). In the context of this time, it means "only" in an extremely common case. The difference matters. People who read the presentation without putting it in historical context will be misled, even if Osterhout weasel-worded his conclusions because he kind of knew he was wrong already.
(OK, in the guts, we did have some utility software that did async disk I/O using a few C threads per heavyweight process, but that was due to the limited kernel API more than us wanting threads.)
I was going to look up the current details for you, but I don't see an SGI Ultraviolet system in the current Top500. How large of a machine do you work with?
Glad to see HPC so represented here on HN, but an HPC counter-example does not diminish an example. To put this another way: I wasn't saying that YOUR problem could be solved without threads, I said MY problem could be solved without threads.
There can be overheads to running very large numbers of MPI processes, depending on how scalable the MPI implementation is, and there are overheads to having too many threads contend for memory on a single node. At least in the current state of things in HPC, there is a delicate balance to choosing the right process-thread ratio, and it differs from application to application and depends on the node count for the job. People struggle to find the sweet spot, but we're definitely not running all MPI anymore.
If you look in your HPC historybook, you'll notice that I was the system architect for InfiniPath.
A bit over-general, don't you think? There are many places that have big clusters full of single-threaded processes. There are also many places that have big clusters full of multi-threaded processes. I don't know which group is the biggest, but it doesn't matter as far as Osterhout's argument is concerned. What matters is that, contrary to the context in which that argument is framed, wringing performance out of multiple CPUs in a single system is an almost universal need nowadays. Many of them furthermore need high levels of intra-process as well as inter-process parallelism for that purpose. Threading is not the only way to achieve that, but he never even considers other alternatives. No matter what hairs are split or who tries to claim the biggest credentials, the OP remains seriously time-bound and its applicability to modern systems is weak.
That said, Tcl was one of the first scripting languages which got very nice thread model - see AOLServer [1].
[1] https://en.wikipedia.org/wiki/AOLserver
We used Tcl threads in one of our programs to control various hardware things while main UI responded to events sent from the threads. Everything worked very well, especially for a program in scripting language like Tcl.
He says the first thing he does, as a performance consultant, is turn off threading. Claims that's often all he needs to achieve the desired improvements...
It's a good talk, I highly recommend it.
A 20 core CPU can do some things 20 times faster than a single core (multiply matrices e.g.). If you're trying to do those things and you limit yourself to a single thread - good luck!
Or taking a event-driven language and retrofitting concurrency, which Javascript is doing. That results in something that looks like intercommunicating processes with message passing. That's fine, but has scaling problems, because there's so much per-process state.
Languages which started with support for both threads and events, such as Go, do this better. If a goroutine, which is mostly an event construct, blocks on a mutex, the underlying thread gets redispatched. There's only one kind of blocking.
Interpreter as a library, all state encapsulated in a reentrant interpreter object, different interpreters running in different threads that can send messages to each other, no need for expensive serialization deserialization (like in Python) because the interpreters are in the same process, ... so much nicer.
On other operating systems, specifically those starting with the letter W, there's a major distinction. There are other constructs as well, such as "fibers".
Now, today's world is different from what it was in 1995. We used to have a single core, so threads and multiple processes were only a logical construct. Now, we have multiple cores, so we shold, at a bare minimum spawn multiple processes/threads. What's running inside them can then be debated as if it were 1995.
There are only a handful of examples, that i can think of, where threading(multiprocessing, concurrency, and other names for it) is useful.
I've programmed using threads for 23 years. I've never had a non-trivial debug issue caused by trouble using semaphores, mutexes, and shared data. It's no harder than writing a hash table or balancing a tree.
Given the number of stories about equator issues and negative altitude issues in autopilots, I'm unconvinced there are proofs involved in those either.
- take mutex
- read/write/modify shared data structure
- release mutex
There are plenty of multi-threaded RTOS and a lot of critical embedded software is multi-threaded. Typically there are clearer guarantees in those vs e.g. Linux. Multi-threaded code isn't fundamentally (from a theoretical perspective) different than single threaded code when it's running on a single core. Multiple cores communicating is sort of orthogonal.
It's reasonable for one person to write a correct multi-threaded program, even a large one. But when many different people start working on the same large, multi-threaded program, correctness becomes much more difficult. People start forgetting which functions and data structures are and are not threadsafe.
That sounds like something which could easily be solved with common sense commenting of code.
Sure, those are easy, but they don't scale. You'll often want non-blocking instead of semaphores for higher levels of parallelism and thats where you'll run into trouble.
People run into deadlocks and other issues when they don't realize what mutex protects which data, or they try to take multiple mutexes, or they hold a mutex while calling a callback, or they don't protect access at all.
From a performance perspective you need to be aware of how much contention you have on a mutex. Typically locking a mutex should never block. If you have contention there are patterns for dealing with that as well.
For most types of concurrent applications you're done. Yeah, lockless techniques, spin locks, etc. may have their place but for the most part keep it simple.
- Dozens of examples where access to a variable shared-between threads was not synchronized. These can be nasty, because often you won't observe an issue at all. And maybe some month or years later weird issues are reported, which can often not be reproduced and where the root-cause is hard to find.
- Deadlocks, due to locks taken in different orders. E.g. in cases where a user-thread with user-code calls in a library and the library calling back into user code from another thread.
- Nasty memory corruption issues. E.g. there was a case where one started a timer (which executed in the background thread) which should manipulate some variable when the timer elapses. In case the timer was no longer needed it was cancelled. Under some race conditions the code in the timer thread ran even after it was cancelled (from the user thread) and accessed memory which was already freed in the main thread. The correct solution would have been in the main thread to wait until the timer has guaranteed to stop (with out without executing it's code) and only to release the memory afterwards. This happened in an embedded system where you get no segfault but just weird behavior. So it was definitely a non-trivial to debug issue on which multiple days were wasted.
- In event-based systems code which blocks the whole eventloop (and thereby slows down the system immensely) by waiting for the result of a long-running operation. E.g. in .NET code you might often find people just using Task.Result without knowing it's implications (or what the difference to await Task is).
My personal experience is that while for some people concurrency and multithreading is quite easy there are also lots of people which have a hard time understanding and utilizing it correctly.
As someone who grew up on the Java VM and started my start-up/web career on it, I've always felt like Java programmers have a different relationship with threads than C programmers. Java gives you cross-platform, usable, debuggable native threads; it basically makes them free if you want them. In C/C++, on the other hand, threads are a library, and using them is a grungy affair. If you grew up on Rails, meanwhile, threads don't exist ("when you say worker thread, do you mean worker process? I'm confused").
Node.js was created by C programmers and launched with a lot of anti-thread propaganda, much like the link. They equated threads with shared state, and also said threads were too memory inefficient to be practical for a high-performance server (they meant that holding a native thread for every open connection would require too much stack space, which is true, but that's not what they said).
The difference is Java threads work within the constraints of the JVM memory model. Which is very well specified and understood. It is implemented in the JVM and the JVM guarantees your platform will follow it.
While C/C++ do what ever the hardware dictates.
https://dzone.com/articles/java-memory-model-programer%E2%80...
This is the point where performance tops: each CPU is filled with operations, and operations that don't need to wait for the result of other threads.
How's that for an honest response lol.
Yes, but only if there can be structural sharing between threads.
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).
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.
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.
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.
If you want to make use of multiprocessing, the traditional choice is either to use two separate processes (sharing nothing), or to use threads (and share everything). But for most tasks, these opposite ends of the spectrum are not what you need. There's plenty of data and state in most programs that doesn't need to be shared, and a huge source of threading bugs is through mistakenly altering some data that another thread was using.
The problem is that sharing partial state between processes is painful and many languages and OSs make it difficult to do. You have to play around with mmap() or other shared memory tools, and then pay great attention to not mix pointers or other incompatible data between the processes.
I think that threads are definitely a bit tricky though since it is easy to mess up locking/unlocking or not lock things necessary and if you do then you have debugging headaches. So when not needed they should be avoided I think.
More seriously, in C++ apps (Games financial trading etc...) it is common enough to need to carefully manage memory to get that last bit of performance even after saturating all the CPUs and GPUs. Superficially your advice makes sense.
Going forward I think I might leave threads in the same mental bucket as allocators and see if that heuristic holds up.
Then you run into issues like that post recently where you can't have different threads running in different context, because the runtime is the one deciding/controlling when new threads are spawned.
And OP actually says that limiting usage of threads to handle blocking stuff is fine.
The phrase "wordpress programmer" conjures up the person for whom that is their sole form of programming expression. Some part of me acknowledges that people like that must exist: the rest recoils in horror.