Top
Best
New

Posted by gw2 4/2/2025

Multi-threading is always the wrong design (2023)(unetworkingab.medium.com)
33 points | 52 comments
pjc50 4/2/2025|
> A CPU with 4 cores is going to have the capacity of executing 4 seconds of CPU-time per second. It does not matter how much “background idle threading” you do or don’t. The CPU doesn’t care. You always have 4 seconds of CPU-time per second. That’s an important concept to understand.

> If you write a program in the design of Node.js — isolating a portion of the problem, pinning it to 1 thread on one CPU core, letting it access an isolated portion of RAM with no data sharing, then you have a design that is making as optimal use of CPU-time as possible

This is .. not true as written? You get one second of CPU time per second, not four. Now, it may be quite hard to reach your full four seconds of CPU time per second, usually because of RAM bandwidth issues despite all the caching, and a hyperthreading fake "core" absolutely does not count the same as a separate die core, but the difference is real.

Author does have a point that slicing the work too small has significant overheads. But they've overstated it.

And this is before we get into the real source of parallel FLOPS, the GPU.

(edit: note that there may also be thermal issues and CPU frequency scaling going on; it is usually impossible to run all cores of a modern CPU at their max rated frequency for more than a very short time! But if you've bought a 64-core Ryzen and are only using one core, there's a huge gap there which you're not using)

UncleEntity 4/2/2025|
> Author does have a point that slicing the work too small has significant overheads. But they've overstated it.

Exactly.

I was messing around with adding multi-threading to this 3d thing and it slowed it down for the smaller cases up until it overcame the overhead then it sped things up. It was using OpenMP and only a couple shared loop variables so probably not as drastic as whatever node does but it did slow the common case down enough to be not worth the effort.

The author of TFA needs to go run any renderer in single and multi-thread mode then report back to the class.

pjc50 4/2/2025|||
> The author of TFA needs to go run any renderer in single and multi-thread mode then report back to the class.

Indeed. The whole of modern graphics API architecture hinges on the idea that each of your million or so pixels is a meaningful unit of work that can be done in parallel.

gpderetta 4/2/2025|||
I still think that the argument is flawed beyond trivially parallel problems, but my understanding is that the author is arguing for shared-nothing, not for single threaded/single-process solutions.
codemusings 4/2/2025||
> The irony is that, since you are splitting the problem in a way that requires synchronization between cores, you are actually introducing more work to be executed in the same CPU-time budget. So you are spending more time on overhead due to synchronization, which does the opposite of what you probably hoped for — it makes your code even slower, not faster.

That is certainly not universally true for every scenario and if you need to sync state between cpu cores very often then your tasks simply don't lend themselves to parallelization. That doesn't mean that multi-threading is inheritely the wrong design choice. Of course it will always be a trade-off between performance gains and the code complexity of your job control.

hinkley 4/3/2025|
But people new to thinking about concurrency don’t know this. They write “parallel” code that goes pretty fast, and that initial reward convinces them to continue. Then the bugs are found, and their parallel code gets slower, but that’s just bug fixes. Never mind the performance loss.
cjs_ac 4/2/2025||
I'm perfectly happy to accept that multi-threading isn't the best solution to certain classes of problems, and the author makes compelling arguments to this end, but they haven't said what classes of problems shouldn't be solved with multi-threading.

That's the fundamental weakness of all these 'best-practice' blog posts: authors are much more willing to pluck a tool from their toolbox and tell us to use that one than they are to give advice on how to pick the right tool for the right job.

boxed 4/2/2025||
> If you write a program in the design of Node.js — isolating a portion of the problem, pinning it to 1 thread on one CPU core, letting it access an isolated portion of RAM with no data sharing, then you have a design that is making as optimal use of CPU-time as possible. It is how you optimize for NUMA systems and CPU cache locality. Even a SMP system is going to perform better if treated as NUMA.

Well that's... just wrong.

jiggawatts 4/2/2025|
It's not wrong, it's just over-simplified.

It is true that you can only squeeze 100% of the maximum possible useful compute out of a NUMA system with methods like the article author was suggesting. The less coordination there is between cores, the less cross-core or cross-socket communication is needed, all of which is overhead.

Caveat: If a bunch of independent processes are processing independent data, they'll increase cache thrashing at L2 and higher levels. Synchronised threads running the same code more-or-less in lockstep over the same areas of the data can benefit from sharing that independent processes can't. In some scenarios, this can be a huge speedup -- just ask a GPU programmer!

Where the process-per-core argument definitely stops being a good approach is when you start to consider latency.

Literally just this week, I need to help someone working on a Node.js app that needs to pre-cache a bunch of very expensive computations (map tiles over data changing on an interval).

Because this is CPU-heavy and Node.js is single-threaded, it kills the user experience while it is running. Interactive responses get interleaved with batch actions, and users complain.

This is not a problem with ASP.NET where this kind of work can simply run in a background thread and populate the cache without interfering with user queries!

For similar reasons, Redis replacements that use multi-threading have far lower tail latencies: https://microsoft.github.io/garnet/

pjc50 4/2/2025|||
Someone familiar with this please confirm: if you have a node.js app, which is true single-threaded, do people run multiple copies per physical CPU, or do they just max out one core and leave the rest idle? Or lease the cores separately from their hosting provider by running a bunch of one-core container instances or something?
gbuk2013 4/2/2025|||
Node supports 2 ways of getting an additional event loop threads (this being the “single-threaded” part that people talk about with Node often without understanding much about its internals, as the Node process itself spawns many threads in the background).

The first mode is child process: the main process forks an entirely separate instance of Node with its own event loop, which you communicate with over IPC or some network socket.

The second mode (introduced fairly recently) is the ability to spin off worker threads which have their own event loop but share the worker thread pool of the main process. I think there is a way to share memory between these threads via some special type of buffer, but I have never used them.

The first mode maps directly to the idea of micro-services, just running on the same machine. This is why it is not really used AFAIK in modern cloud based apps, with single core micro service instances used instead. This approach has a higher latency cost but allows cheaper instances and much simpler services - it very much depends on the use case to decide if that is correct choice or not.

Jarwain 4/2/2025|||
At my company we use node's cluster module, configured to effectively run a child process for each physical CPU on our Azure App Service.

We have a load balancer in front of that to scale app service instances.

jiggawatts 4/2/2025||
I mentally translate “modern web development” to: proxies for the proxies and layers of load balancers upon load balancers to make the envoys work with the ingress, all through an API management layer for good measure… and then a CDN.

“Why is our app so slow?”

“It’s a mystery. Just scale it out some more!”

hinkley 4/3/2025||||
It’s a common architecture antipattern to try to run non-interactive tasks on the same resources as interactive ones.

Even if you avoid the initial problems by punching the CPU priority through the floor, someone will eventually introduce a bug/feature that increases IOPS or memory usage drastically, or finds some other way to accomplish priority inversion. You will think you can deploy this code any time you want and discover that you can’t.

And having survived the first and second crises, you will arrive at the final one: a low-priority process is not a no-priority process. People will expect it to be completed “on time”. And as the other processes saturate the available resources via either code bloat or right sizing, now your background task isn’t completing the way people have come to expect it to complete. It stops being able to be “free” by parasitism and has to have its own hardware.

boxed 4/3/2025|||
> It's not wrong, it's just over-simplified.

I would say if you simplify this much it becomes just plain wrong.

EVa5I7bHFq9mnYK 4/2/2025||
And the reason given is:

"it brings complexity very few developers understand"

Someone tell this guy about GPUs ...

andy24 4/2/2025||
Or games. Or web servers. I think the author might have forgotten to suffix the title with “for the work I do”.
packetlost 4/3/2025||
Web servers are notoriously easy to implement with message passing / fork. Threading is a particularly bad model for that case.

Games on the other hand? Oh yeah, you definitely need threads for that.

It's important to note that "multithreading" specifically means shared-memory model of parallel processing. Games are more of an exception than the rule when it comes to being well suited to shared memory.

andy24 4/4/2025||
Isn’t Nginx multithreaded rather than multiprocess? I’m not an expert in web stuff but it’s always felt intuitively that worker threads map nocely into typical web server workloads.

Also, any UI app should basically be multithreaded to prevent interface hiccups.

packetlost 4/5/2025||
No, nginx uses a fixed number of worker processes and communicates primarily through explicit shared memory and message passing.

Threading works ok, but there's rarely much shared state so the fork and pipe / she model wins cleaner and more secure.

As for UIs: maybe! I'm sure it depends, but this isn't an area I know a ton about

andy24 4/6/2025||
Interesting perspective, I’ve never been thinking about this problem in terms of whether workers need shared memory space. Thank you!
wiz21c 4/2/2025||
yeah, definitely. I do GPU coding at work and that's hell. Optimizing the memory saturation and cpu saturation is maddening and always comes with surprises.

(but when it works, it's so gratifying! the GPU go very very fast)

jabl 4/2/2025||
Not sure why this is on the HN front page. Post is riddled with errors, although it mixes in a few in-some-cases truths as well.

Oh, and [2023].

Kiro 4/2/2025||
This guy's patronizing rants has made me avoid his software (µWebSockets) even though it seems good. Stop trying to be Torvalds. Even the docs used to be riddled with it:

> In the 1970s, programming was an elite's task. Today programming is done by uneducated "farmers" and as a result, the care for smart algorithms, memory usage, CPU-time usage and the like has dwindled in comparison.

t43562 4/2/2025|
Forgetting the tone for a moment but I can say I see lots of people using things they don't understand and making hard problems for themselves. To be fair I know that I don't understand those things properly and yet see people rushing in to use them who know even less.

With threads it was a messaging service that was supposed to offer a persistent queue that could survive restarts. It possibly doubled or trebled the length of the project through multithreading bugs. I wasn't the creator of the code - it was someone without a degree. In the end I had to solve one bug on it that took 3 months to work out and that was just a double-free in some odd circumstance. Nobody wanted to touch it and muggins (i.e. me) was the last person without an excuse!

Asynchronous python and python threading are the recent ones I've experienced - javacript programmers who decided that python was trivial to learn and tried to speed everything up with threads (makes everything worse until 3.13 with a special compilation option that we could not use) and then they made life even worse to no purpose at all by using async without knowing that the ASGI system underneath didn't support it properly. Uvicorn does but uvicorn wasn't usable in that context.

Apart from creating wonderful opportunities for bugs they didn't even know how to write async unit tests so the tests always passed no matter what you did to them.

When trying to help with these issues I found the attitude to be extremely resistant. No way they were going to listen to me - the annoying whippersnapper in one case or the old fart programmer in the other. They just knew better

invalidname 4/2/2025||
Multi-Threading is the worst solution, except for all the other solutions.

Avoiding multi-threading doesn't remove concurrency issues. It just moves them to a different point in the application execution. A point where you don't have debuggers and need to create overly fault-tolerant behavior for everything. This is bad for performance but worse for debugging. With a regular synchronous threaded application I have a clean, obvious stack trace to a failure in most cases. Asynchronous or process based code gives me almost nothing for production failures or even regular debugging.

Snoozus 4/2/2025||
Using something you don't actually need is wasteful. When you don't need threads, don't use them. Trivially parallelizable problems should be trivially parallelized.

Sometimes you want to do something complex in a short time though. If latency matters, sometimes you don't have a choice. Running 10 instances of the same game at 10fps each is not the answer.

varjag 4/2/2025|
Bit dizzy from the praise of Node.js and complaints about cache invalidation in one rant.
More comments...