
Should I Use Threads? - djsumdog
https://shouldiusethreads.com/
======
pubby
There should be a website that tells people to not make generalized
statements.

Besides, I think the author is missing the point. Sharing memory between
threads is a huge part of what makes them fast. The difficulty doesn't lie in
having to deal with mutexes - in fact I think mutexes and critical paths are
easier to work with than message passing. Instead, the difficulty lies in
trying to weave all these parallel moving parts together in a way that's both
fast and correct. Synchronization primitives are easy, it's the actual
synchronization that's hard, and multi-process doesn't fix that.

~~~
deckarep
Bingo, generalized statements like this help no one. It’s a very narrow-minded
view of software engineering. Instead the author should adopt a more pragmatic
view of the world of coding. We don’t live in a black and white world and
software is no exception. Use the right tool for the job and remember
everything has tradeoffs.

~~~
rwoerz
"generalized statements like this help no one."

May I cite this when I explain recursion?

SCNR

------
MaulingMonkey
> You SHOULD NOT use threads.

You have no choice.

Graphics drivers fire up threads. Audio libraries fire up threads. Xinput
fires up threads. Your library will be used in multithreaded programs. "I have
an executable!" you might object - but this will be rebuilt as a library to
accomplish involuntary multithreading if need be, or just loaded like a DLL
as-is. Anti-viruses will inject threads. Corporate monitoring tools will
inject threads. Debuggers will inject threads. You'll need to be able to
initialize COM cleanly without worrying what the impact on other COM APIs the
current thread could be using might be. You'll need to turn a blocking API
into an event loop or other asyncronous style. Android fires up separate UI
and Rendering threads even if you don't want them, and IIRC prohbiits some IO
on the rendering thread. WinRT's main UI thread isn't the "main" thread that
ran the entry point.

You can avoid firing up a single thread and _still_ end up debugging
multithreading bugs. Given that you're going to have to go through the effort
of making decent chunks of your code multithreading safe anyways, you might as
well use some of the easier tools for the embarassingly parallel parts of your
program for some easy performance wins.

~~~
zvrba
A fun fact: When you press CTRL-C in a Windows console program, a thread gets
injected into the program. [https://docs.microsoft.com/en-
us/windows/console/ctrl-c-and-...](https://docs.microsoft.com/en-
us/windows/console/ctrl-c-and-ctrl-break-signals)

------
DmitryOlshansky
This is so weak and misguided on multiple levels.

A thread is virtual conterpart of the real hardware _thread_ of execution.
Without a concept of threads you have nothing in the high-level side to map
hardware to. Think again before posting this kind of material, it seems to me
vaguely embarrassing.

Now going upper to the place that you probably find the most familar - a user-
space. Truth be told if the app is a unikernel there is almost no overhead in
switching threads (just switch the stack and update a bunch of registers). So
for unikernel they are perfect abstraction, but you may want to add queues,
channel etc on top.

Getting to the user/kernel classic OSes - again the problem is lack of trust
between user-space and the kernel, the hardware can still switch things quite
fast provided we can share most of memory mapping between the two. Nowadays
kernel bypass techniques and eBPF running as trusted scripts in the kernel, I
do see anything wrong eith threads per see.

The architecture pattern of just spawning one new process or thread per
connection is bad only if the said processes or threads are expensive to
context switch between, and that cost is a rapidly moving target.

TL;DR: please stop writing and start reading, there is a lot of homework to do
here.

------
deckarep
If I were to have taken this advice when I first heard it...I wouldn’t have
had the pleasure of working with the intricacies, ins and outs of a threaded
model.

This is what I’ve heard through my career: don’t use threads, don’t use locks,
don’t use pointers, don’t use languages with manual memory management. The
list goes on: don’t use unsafe code, don’t use pointer arithmetic...

The bottom line is that these concepts can be used successfully if you
understand the traps. Yes a sufficiently large codebase will eventually suffer
from things like data races, memory leaks, etc but if you always take this
advice and avoid the dark side of software development you’ll never develop an
aptitude for how to deal with such code when you see it the wild.

Instead of saying don’t use threads I would say. Learn the good and bad of a
threading model. Know your limits, and study the traps so you can possibly
avoid them. Still there are no guarantees but all languages have warts if you
look deep enough even high-level languages.

Take Go for example, it has a threaded model with m x n goroutines scheduled
over threads. Guess what? Go can easily have data races if you aren’t careful.

It’s still a great language! Still worth learning.

------
ebg13
> _Look at your program 's CPU usage. If the sum of your threads is <100% of
> one core, then there is no reason whatsoever for you to use threads. If your
> program is not CPU bound, then you would be infinitely wiser to use an event
> loop1 or coroutines2. If you are not CPU bound, then the entire value
> proposition of threads does not apply to you._

Ok. Give me a non-thread-based solution in Python that can parallelize IO-
bound tasks as easily, universally, and naturally as throwing in 3 lines of
concurrent.futures.ThreadPoolExecutor and concurrent.futures.as_completed.

~~~
therealdrag0
Ooh I didn’t k ow python had this functionality! Kinda nice they reused
existing vernacular too!

------
begriffs
Well that was certainly a lot of fluff and FUD.

OP says:

> Look at your program's CPU usage. If the sum of your threads is <100% of one
> core, then there is no reason whatsoever for you to use threads.

Attacking a CPU-bound problem with parallelism is only one reason for using
threads. Concurrency (even on one processor) has benefits.

> The shame is in believing ourselves super-human, and reaching for a tool
> that we don't need, in full knowledge that we're likely to shoot ourselves
> in the foot with it.

Although synchronization primitives require care, they're not _that_ hard. You
don't need super-human powers.

I took some time to understand pthreads and apply them to example programs.
For a careful and balanced overview, see my article:

[https://begriffs.com/posts/2020-03-23-concurrent-
programming...](https://begriffs.com/posts/2020-03-23-concurrent-
programming.html)

~~~
syspec
Great blog entry!

------
andr
This is naively bad advice, in that it doesn't educate the reader, rather it
just creates stigma.

Threads are a fine tool for many jobs. You just need to understand what you
are doing. Reduce your shared state. Use locks when more than one thread may
write data simultaneously. Use queues/channels for message passing. Profile
your lock contention when things get slow. Learn a functional language, so
that you get the hang of writing no-side-effect functions and using immutable
data structures. This will immensely help you with writing concurrent code.

You should use threads.

------
cozzyd
What about when you want shared memory between threads?

Sure a pipe works in the unidirectional case, but what if you need to
communicate back? Signals are a thing, but they're worse than threads.
Multiple processes and shm_open is a thing, but is that really easier than
multithreading? (You still need mutexes, and such, but now they're across
processes... yuck!).

Also, for many use cases, "#pragma omp parallel for" is often easier than
anything else.

------
jpz
We all used co-routines in Windows 3.1 ("co-operative multitasking"), and the
application Window would go blank. Then we got threads and we were able to
kick off compute to the background without having to be careful to yield
throughout our background task.

For instance, of a spreadsheet computing values.

A more common use-case, however, is of trivially parallelizable problems which
are completely isolated. Consider Monte-Carlo simulation for instance, where
the thread hand-off to workers is done by a queue.

Making categorical statements like this political and unhelpful, and is one of
the things that annoys me the most about IT culture.

~~~
pdonis
_> We all used co-routines in Windows 3.1 ("co-operative multitasking"), and
the application Window would go blank._

Yes, insisting on piping everything through a single message loop, including
tasks that block, is not a good idea.

The really bad part is that even though we're now on Windows 10, and Windows
has claimed to have preemptive multitasking for a number of versions now, you
can _still_ see Windows applications lock up and their windows go blank when a
blocking task happens that stalls their message loop.

If there were ever a use case for threads (what? they can't run the GUI in a
separate thread from background tasks? This is 2020, people! Even Javascript
knows how to do this!), this is it.

------
saagarjha
Spinning up a new process is much more overhead than creating a thread.
(You're going to execve, right?) Since you're usually reaching for threads
when you would like extra performance, in this case creating a new process and
dealing with its issues (how do I synchronize execution? How do I share data?)
can be quite prohibitive.

(I should also note that the tooling for debugging and analyzing multithreaded
programs has gotten much better recently, and claiming that language designers
are lying to you and pinning the blame on runtime bugs is somewhat bogus since
such bugs are clearly way outside the realm of why you should not use
something for almost all applications, and affect non-multithreaded programs
as well.)

~~~
moonchild
A lot of the time, you can set up your shared state ahead of time and then
fork. Still likely more overhead than just creating a thread, but not as much
as an exec.

Ultimately, the question of how (and when) to parallelize is very domain- and
problem-specific. Multithreading, multiprocessing, coroutines, and xargs -P
will all be the right solution sometimes.

~~~
saagarjha
And then you lose most of the state on the execve you inevitably perform
afterwards?

~~~
moonchild
No; the point is that you never exec at all.

~~~
saagarjha
There are very few things you can legally do after a fork.

------
acqq
It's as useful advice as:

"Should one use arms when swimming? NO! Because that can make you swim faster,
and that's dangerous because you'll get to swim where you wouldn't with the
arms bound on your back."

Explanation: if you haven't learned and understood enough of computer
architecture, computer programming, how the operating systems work, how the
libraries you use work etc, yes, please, don't use threads. Most with that
background, who do simply lack the required knowledge to use them.

And if you have knowledge and experience and there is a real problem having a
reasonable solution when threads are used, you don't need a site like above.

Moreover, one can acquire both knowledge and experience. But, that process,
I'm quite sure, does not depend on clicking on a site having the content like
the above and believing it.

~~~
ygra
Many languages and their standard libraries also have useful high-level
abstractions over threads and thread-pools that are much safer to use.
Currently whenever I see a manually-managed actual _Thread_ instance in a
program I'm wondering whether that's really necessary and for things where,
say, you just want to start a somewhat long-running computation and get
notified about its completion and result, while your UI stays responsive, I
wouldn't voluntarily use a bare thread for that.

~~~
acqq
I never questioned that, I of course use the abstractions myself, when
convenient. Where have you received an impression that I promote "actual
Thread instance" or the use of any specific language construct explicitly? I
haven't written anything like that.

However, even if the abstractions exist, that doesn't mean that understanding
the lower levels, or what is abstracted and what not, is not necessary. Those
who don't know what actually happens often have wrong expectations, and design
their code based on them. Then only later it turns out that the expectations
were simply wrong, and not that the OS or the libraries or the languages made
some false promises.

Often, the most interesting information is not what are the abstraction
primitives but what from lower levels is not covered by these abstraction
primitives, or how these abstraction primitives map to which lower levels.

~~~
ygra
Oh no, this wasn't a rebuttal; it was merely an addendum to the points you've
made. The site seemed in some places to advocate against not using threading
at all, for reasons that have more to do with how easy it is to shoot yourself
in the toes with a bare thread, while the abstractions on top of it are
usually much easier to handle.

------
dependenttypes
Honestly, pretty much the only issue with threads is that people abuse them
when they want state machines instead. When the target is parallelism however
there is nothing wrong with using threads, or when you use synchronous apis.
The fact that he suggest for coroutines shows that the author is misguided and
part of the problem.

------
alex_young
“It depends” is too many letters. Just put “no”, that will get views.

------
s9w
That seems like a very strange point of view. Sharing data is a bonus, not a
burden.

~~~
cozzyd
And in cases where you don't need shared data, it's not that hard to get
threading right.

------
monocasa
I agree that they're the wrong architectural primitive almost all cases. If
you're doing "new thread()" in the meat of your code's business logic, you've
lost.

There's a lot of reasons why you want threads though that aren't your CPU
hitting 100%. Less janky UI for some systems is one of them.

------
nilkn
I generally agree with this, but it neglects the existence of the class of
problems that are CPU bound but don’t just split up neatly into separate and
independent workers. For such problems trying to use multiple processes can be
considerably harder (and far less performant) than just using multiple
threads.

------
jakelazaroff
_> Look at your program's CPU usage. If the sum of your threads is <100% of
one core, then there is no reason whatsoever for you to use threads._

You’re doing a core–maxing CPU–bound computation and you want your UI to
remain responsive at the same time.

~~~
trog
How much is this still a problem in a multicore world? I guess I assume my UI
is being managed by a separate process on a separate core most of the time,
and any userspace stuff that is demanding CPU will just be thrown onto another
core. I guess one issue is that you might end up with a single core dedicated
to UI so not running at full utilisation, taking advantage of threading?

------
nitwit005
> A good model is an overseer program, which organizes the work to be done and
> aggregates the results, spawning worker programs to run the actual
> computations. This is not only much more robust, but it scales better, since
> you could distribute these programs across multiple computers later on.

Splitting the application into a master process and child workers does not
mean your app can magically distribute its work across machines, or gain
performance from doing so.

It might sometimes be easier to convert such an application to a distributed
setup, as at least it contains some concept of IPC, but I wouldn't make any
guarantees about that.

------
f2000
Reminds me of circa 1998 when people said "don't use Javascript" because
mixing logic with presentation is the root of all kinds of evil. I have used
threads for decades now and am still alive to talk about it.

~~~
collyw
Having done plenty of JavaScript maintenance I have to agree with them!

------
stefanchrobot
A much better suggestion would be to look for a language/VM that supports a
better concurrency primitives, like Elixir/Erlang. Obviously you can't run
that everywhere, so sometimes you'd have to deal with threads. But after
giving Ruby an honest shot at writing a backend system and hitting concurrency
bugs (in an external gem), I'd rather avoid threaded programming model
altogether.

~~~
jpz
This is another categorical statement. I love how the languages you mention do
concurrency, but I don't think that if you use threads, one should only be
doing it in Elixir/Erlang (or go).

I find that nearly as disagreeable as the original posting.

~~~
stefanchrobot
I'm categorical only in terms of my personal preferences. Threading model is
just hard, so in general, I'd suggest looking for alternatives. But that's not
a universal advice as there are cases where it does not apply or the
developers are up to the task of handling multiple threads.

------
hyko
Yes. You need to understand them.

The proposed solution doesn’t work for every multithreaded environment, e.g.
mobile. You should instead work with threads at the level of abstraction that
your skill supports and your spec requires.

Some aspects of programming are complex and unintuitive, and that’s OK. If we
just wall off those options then–absent a CS breakthrough–a whole class of
programs would not exist.

------
devadvance
This makes for an amusing read, but it would be helpful to have evidence.
While threads (kernel or user space) have their tradeoffs, there are tradeoffs
to using multiple processes as well. What's the cost?

> On a long enough time-scale, all programs will be proven incorrect. All code
> has bugs, including your compiler and language runtime.

That's a bit of a false dilemma. Kernel bugs exist too.

------
jstrong
God, I hate this attitude.

------
zvrba
Oh what a load of biased crap in that article.

> then you would be infinitely wiser to use an event loop or coroutines.

Coroutines... if you're lucky that your PL of choice supports them in a non-
hacky way. C# kind of does with async/await (it's customizable how they
behave). Event-loop replaces one evil with another; rewriting otherwise
sequential code as a series of callbacks, or (better, somewhat more
maintainable) a state machine.

In both cases, one problem is replaced with two others: The first is you have
to be careful to not block for a long time in event handlers (or even
accidentally invoke blocking I/O that may be hidden in some library you happen
to be using). The second is debuggability if your language and tooling doesn't
have native support for coroutines; look up "stack ripping".

And then, to top it all, he forgot to tell you that most event loop frameworks
allow you to run an event loop on each core, i.e., multithreaded, so you have
both events and threads.

> A thread has access to the same address space as other threads in your
> process, and can do whatever they want with it.

The same holds with any event handler invoked by the event loop. An event
handler can just as easily corrupt data belonging to a "wrong" session and the
problem will manifest itself 10 minutes later.

> You have to take extreme pains to avoid accidentally doing the wrong thing.
> You'll probably mess it up, and the symptoms will show up in an unrelated
> part of the program 10 minutes later.

These sentences assume that whoever is reading this is an ignorant fool. There
are tons of books and papers, which have also trickled into mainstream PLs,
that teach about writing well-structured and safe threaded code, i.e., there
exist patterns for many typical problems.

Actually, I'd argue that with Java and C#, and to a lesser degree modern C++,
it's actually _easy_ to write multithreaded code if you think about
structure/design first.

You're entering "experts only" area when you start thinking that "mutexes are
expensive" and to replace them with interlocked operations.

> But, perhaps your problem is CPU bound. In that case — you still shouldn't
> use threads! Consider using multiple processes instead.

Oh yes, "great" advice. So you have to decide how these separate processes
should communicate.

If you're going for shared memory, you're back to square one, except that
sharing is cumbersome because data is mapped to different addresses in
different processes, so pointer-based structures can't be shared... At least
C++ has a Boost library that mimics the usual containers (vector, map, set) by
using special offset-based "pointer type" so they can be safely shared. And
you _still_ must synchronize access to shared data.

If you go for the no-shared approach, well, you have to send data through
socket, pipe, or a message queue. So you have to design some marshalling and
communication protocol (or use a library), but the major drawback here will be
performance: each send/receive requires a user-kernel transition, AND, copying
data between user- and kernel-space. Twice (once from sender -> kernel, the
other time kernel -> receiver).

If the MQ implementation is clever (like on Solaris), the communication buffer
will get mapped into both processes so you can get away with copying the data
only once, provided the MQ allows you to "peek" and consume data without
copying it out from the shared buffer first.

> This is not only much more robust, but it scales better, since you could
> distribute these programs across multiple computers later on.

This is presented as if distributing across machines is simple matter. What is
not mentioned is that communication between processes on the _same_ machine
will NEVER fail. As soon as you have network, random latency increases or
network outages will kill performance or expose a whole new class of bugs.

> Second, your language designers are lying to you. On a long enough time-
> scale, all programs will be proven incorrect. All code has bugs, including
> your compiler and language runtime.

Sure. So go and write in assembly.

------
networkimprov
You should use _green threads_!

[https://en.wikipedia.org/wiki/Green_threads](https://en.wikipedia.org/wiki/Green_threads)

------
rock_artist
Threads aren’t only for maxing out cpu(s) usage. You can already use SIMD (or
libs/compiler that uses it under the hood)

Threads main use case IMHO is responsiveness. You might want to avoid creating
new ones for no reason. But in common desktop or mobile app you’ll find
yourself in need for running some code NOT on the main thread (unless you love
your users feeling the app is stuck).

------
IshKebab
What nonsense.

------
jake_morrison
This was sort of true when computers had few CPUs/cores. It didn't make much
of an efficiency difference, it was mostly a question of programming model. It
works fine as long as the program is fundamentally I/O bound, but breaks down
when it is doing actual work and becomes CPU bound.

I have certainly had some horrible experiences debugging multi-threaded C++
programs, and using non-blocking I/O made them safer and easier to reason
about because only one thing happens at a time. The non-blocking I/O
programming paradigm, however exposes unnecessary "plumbing" to the
programmer.

What we really want is a concurrency model where we can handle each request as
if it was the only thing happening.

To the extent that multiple requests need to share resources, we need ways to
safely access state with concurrent access. That might be a relational
database, mutexes, or software transactional memory.

I am currently in the middle of updating a large application written in
Twisted Python. async/await and promises can certainly be complex. The Flask
approach of using one thread per request is easier to understand. Everyone in
the Javascript is in love with async/await, but I can see where that ends:
just look at what Twisted was doing 10 years ago. And see that it basically
failed due to complexity.

The model I like best is Erlang/Elixir. The runtime handles I/O using non-
blocking I/O and threads to be efficient. The programmer works in terms of
lightweight actors (called "processes", more like Java "green threads") that
communicate using message passing. Each individual Erlang process only does
one thing at a time.

As Joe Armstrong, one of Erlang's inventors, says: "We do not have ONE web-
server handling two million sessions. We have two million webservers handling
one session each." [https://joearms.github.io/published/2016-03-13-Managing-
two-...](https://joearms.github.io/published/2016-03-13-Managing-two-million-
webservers.html)

Message passing handles concurrency without locks. If multiple client
processes send update messages to a server, the server simply pulls them from
its inbox in order and updates the shared state it is responsible for. The VM
schedules processes preemptively, handling millions without breaking a sweat.
It's safe, because one process can't mess with another process's memory. It is
easy to reason about and debug.

It easily takes advantage of all the CPUs on the server. You can run a single
VM instead of breaking up your application into multiple servers, each of
which only uses one CPU (via containers or OS processes) and trying to
coordinate between them. The VM has a built-in key/value store with efficient
concurrency. It's like Redis but without the serialization overhead, so a read
takes 1 microsecond.

