21 ms·
Threads Are a Bad Idea for Most Purposes (1995) [pdf]
- rongenre 7y agoRaw threads are really hard to program correctly, however sinking parallel code into an executor or queuing framework tends to really reduce complexity and in a lot of cases get all the cores working.
- dang 7y agoA thread from 2017: https://news.ycombinator.com/item?id=14547063 https://news.ycombinator.com/item?id=14547063 Way back in 2008: https://news.ycombinator.com/item?id=399670 https://news.ycombinator.com/item?id=399670
- zevv 7y ago> A thread from 2017 Didn't you read the article?!
- electricityUser 7y agoDid you click on the links in the comment above yours? ;-)
- metalliqaz 7y agoWhoosh
- birdyrooster 7y agoThe perniciousness of winky face is on full display here
- justincredible 7y agoWinking is now a microaggression ;)
- jeffdavis 7y agoThreads have some very practical advantages: * Standard, easy way to get a backtrace * Standard, easy way to get a list of active things going on * In many cases, threads make it easy to follow control flow
- chriswarbo 7y agoYet threads make backtraces and control flow much less useful, since they miss out important context from concurrent threads. I wouldn't personally say that threads make control flow easier to follow: we might gain a little by disentangling separate activities into threads, but we lose a lot when these get interleaved in arbitrary, non-deterministic ways.
- derefr 7y ago> Yet threads make backtraces and control flow much less useful, since they miss out important context from concurrent threads. I mean, when a program panics, you get a stacktrace from (a consistent snapshot of) all the threads, so what's the problem?
- erik_seaberg 7y agoIf a highly concurrent service gets a query'o'death, it's hard to tell which threads were working on it and which were merely within the blast radius. Frameworks tend to roll their own notion of "request context" without it being strongly typed or pervasive across the language and libraries.
- derefr 7y agoNow I’m curious what backtraces from Postgres look like when parallel scans are enabled. IIRC, you’ve got one isolated fork(2)ed master for the connection, which then has threads to divide work. Not too bad to debug.
- gardnerbickford 7y agoA snapshot doesn't show the shared memory changes that may have caused the panic. There needs to be a synchronous log of all changes to shared memory to debug an issue. Similar to having a snapshot of network traffic vs a recording of network traffic.
- sproketboy 7y agoWon't need them since we'll have Fibers in Java soon.
- bluejekyll 7y agoI think this was truly the most amazing thing about learning Rust. After having experienced the pain (the issues brought up in the linked slide deck) of threads in C, then C++ and Java, it was wild to work with a language that provided some significant safety rails for working with data across threads. Now async/await gives us even better options on top of that, but it’s truly what made me enjoy the language so much. This article is what resonated with me and got me to invest so much spare time over the last 5 years in working with Rust: https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.html https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.h...
- BubRoss 7y agoI think the idea that finding ways of working with threads (and ownership) with more safety while minimizing performance impact is a healthy side effect of rust. I think though that rust is one approach of many and that they aren't orthogonal. I personally think libraries for multi threading on top of rust or C++ are necessary to really nail the problem down. Even async and await are too granular and difficult to get right in my opinion. I think regardless of the language, queues of data chunks (not just tasks) combined with solid concurrent data structures will go a very long way. Rust may help get those libraries and data structures to be more correct with less effort, but ultimately getting threading right at the lower level just isn't practical for most programmers most of the time, and I say this as someone confident in their parallel programming skills.
- acheron9383 7y agoYeah, there are some pretty solid crates coming up. Obviously Tokio as a runtime, Rayon for parallel iterators,and the whole Async Std crate. Rayon is a good example of a crate that helps add concurrency without much headache for embarrassingly parallel operations. Even still, even in languages with built-in runtimes and GC like GO, it is very easy to create deadlocks/livelocks. Programmers intuitions of concurrency are generally wrong, and the correct intuitions are complex. I don't know how we make concurrency easy. I do like Futures however, they provide a nice abstraction over threads.
- bjourne 7y agoIn my life I must have written at least a few million lines of code. Probably more. How many of those lines have been explicitly multithreaded? A few thousand lines, at most.
- cjfd 7y agoSure, only use threads when you actually need them. The cases where you actually need them are quite numerous, though. E.g., you have a library that connects to something over the network but the library is blocking. Or you need to respond to events quickly but for some events you need expensive calculations. If you can get away with copying data to the threads so shared state is minimized this will help. The general principle is to make things as easy as possible instead of as difficult as possible.
- aloknnikhil 7y agolibuv (http://docs.libuv.org/en/v1.x/design.html http://docs.libuv.org/en/v1.x/design.html) is pretty much this in action. Event loops are not necessarily easy to debug though and message passing between event loops will force the use of concurrency primitives such as mutexes and/or memory barriers (lock-free) anyway. Also, working with event-driven architecture requires a more functional approach since the handlers are all short-lived. Reminds me of Erlang. I think the deck downplays the complexity of building such a system.
- davidw 7y agoThe author of the deck is Dr. John Ousterhout, who wrote Tcl, which had an event loop way back in the day.
- dirtydroog 7y agoOh no! callbacks! Hell, etc /s
- pooya13 7y agoIn C++ it has become way easier to manage some of the issues mentioned since 1995, namely by using atomic, future or even locks.
- TheFiend7 7y agoIMHO I think one large part of it is synchronization. If you're having to synchronize things all the time, you're probably misusing threads and should be using a different execution model.
- desc 7y agoAnother way to look at it is that only infrastructure should be locking stuff, and infrastructure should be a very tiny part of the codebase. That infrastructure should probably be responsible for layering a different concurrency paradigm on top of threads... Most platforms these days provide such things as part of the language, or in the standard library, or as a freely-available package. Writing one's own concurrency infrastructure is usually unnecessary, but when it is needed, it needs to be kept as small and as easily-auditable as possible. A bit like `unsafe` in Rust, in fact. Locking all over the place generally indicates that someone's trying to shotgun-debug concurrency bugs. I've had to use libraries which did that, and wished horrible things upon those responsible.
- davidw 7y agoThe author, in case anyone didn't recognize the name: https://en.wikipedia.org/wiki/John_Ousterhout https://en.wikipedia.org/wiki/John_Ousterhout Specifically, he created the Tcl programming language, which had a nice event loop way back when.
- Uhhrrr 7y agoIt's still there! IIRC you could also have arbitrarily many nested event loops, for better or worse. And some threading support is also available [1], although it seems to be culturally disapproved of. 1. https://wiki.tcl-lang.org/page/thread https://wiki.tcl-lang.org/page/thread
- davidw 7y agoI just meant to say that it was already in place... uh...holy crap... 25 years ago.
- kevin_thibedeau 7y agoTechnically it was part of Tk. It got grafted into mainline Tcl 20 years ago.
- topspin 7y agoI solved some tough IPC problems using the TCL event loop. A product that runs on several thousand machines across the US was designed around a set of distinct C++ processes communicating over sockets. Using the TCL event loop greatly simplified the design, improved performance and eliminated concurrency bugs present in the bespoke IPC code it replaced. It should have been written that way on day one but some programmers default to pounding out a bunch of spaghetti instead of learning about the tools at their disposal.
- awinter-py 7y ago> Threads should be used only when true CPU concurrency is needed > Scalable performance on multiple CPUs the exceptions to 'when to use threads' in 1995 sound like SOP these days
- bcrosby95 7y ago> Threads should be used only when true CPU concurrency is needed. Which is basically any program running on any modern processor.
- AnimalMuppet 7y agoNot at all. There are multiple cores, sure, but why does my program have to use them? If it performs adequately using only one core, and if the nature of the problem doesn't require threads, why should I make it multithreaded just because the processor has multiple cores?
- cdoxsey 7y agoYou have two choices when it comes to utilizing multiple cores: threads and processes. Threads are hard, but sharing data between processes is hardly any easier. Maybe they're a bad idea, but these days you have no choice but to learn how to use threads. AMD's run-of-the-mill processors have 16 cores, Intel's 8. Servers have lots more. Heck even your iPhone has 6. The clock rate on CPUs isn't getting any better. It's just more cores from here on.
- jovial_cavalier 7y agoHe says at the end that if you want true concurrency, use threads. But the point is that many people use threads where concurrency is not a requirement, or even desirable.
- deleted 7y ago[deleted]
- chrisseaton 7y ago> But the point is that many people use threads where concurrency is not a requirement, or even desirable. reply Why would you write a program using threads if you don't require concurrency? The only purpose of threads is to achieve concurrency. Can you give some examples?
- plapetomain 7y agoThink of an object oriented system. You can have a thread per object at the extreme where each object has its own thread/queue to handle messages. For most cases with synchronous calls you’re not really getting any concurrency. Maybe it’s hard to imagine now, but in the 80s and 90s there were people that pushed this sort of architecture with a straight face. Even if not this extreme the idea of using threads for componentization rather than a focus on concurrency..which was possibly a side benefit was very much a thing (think COM/CORBA)) Hence why many articles like this and Ousterhout from the 90s, etc saying it was idiotic.
- 7y ago
- oppositelock 7y agoI started doing massively parallel programming on SGI systems around the time this paper was published. SGI's at the time could have 64 CPUs in a single system image, which was very novel. Sun was working on its early multi core workstations, and companies like Cray were pushing different models of distributed computation. This paper came at a time when threads were really painful to work with. POSIX threads were still new and mostly unsupported, so you were stuck with whatever your OS exposed. On IRIX, you would do threads yourself by forking and setting up a shared memory pool, on Solaris, you had the best early pthread support, in Java, you used the native Thread classes which only really worked on Solaris at the time. It was a mess! This mess is now solved. Pthreads are everywhere, C++ has std::thread, Java threads work everywhere, and we've had many new language some out which handle parallelism beautifully - for example, the consumer/producer model built into channels in Go is very elegant. The odd one out is Win32, but it's close enough to pthreads for the same concepts to apply. Event driven programming has also become threaded, this is how the whole reactive family of frameworks, node.js, etc, handle parallelism. As someone who's been doing this for a long time, what I find confusing is higher level constructs that try to hide the notion of asynchronous operations, such as futures, promises. As a concept, they're fine, but they're difficult to debug because most developer tools seem not to care about making debugging threads easier.
- gdubs 7y agoCurious what kind of work your were doing? I’ve always been fascinated by the golden days of SGI.
- oppositelock 7y agoAll sorts of real time graphics stuff; flight simulators, oil and gas visualization of voxel maps, general industrial visualization. These were the early days of hardware accelerated 3D and everyone thought that visualization would change the world to a greater extent than it really did. It was great fun to write code which drove 8 displays using 8 graphics pipes, with roughly 8 cores working in concert with each pipe. All this work for something that runs faster on the latest iphone using a single thread... I went on to work at SGI for a few years, and it was still my favorite job ever. It was pure R&D, graphics and realtime systems for their own sake. Today, this doesn't exist. 3D graphics are an applied technology that's part of an app, but not a product research area of its own.
- anaphor 7y agoIt's not that threads are necessarily a bad idea (though they can be for performance reasons), but that programming with most synchronization primitives is a bad idea. If you program with message passing, then it's not much different from the "events" model except that in the event-driven model you're trying to hide the underlying abstraction more (you still have concurrency, it's just baked into the I/O library). I honestly think this presentation is confused. "Concurrency is fundamentally hard; avoid whenever possible" seems to go against their own argument. Event-driven models (which rely on message passing) are still doing concurrency, except instead of using locks and semaphores to synchronize things, you're using mailboxes and channels. Even CPU interrupts are a form of concurrency that is similar to event-driven models. Just because you're not spawning a thread and acquiring a lock, doesn't mean you're not doing concurrency.
- pkolaczk 7y agoEvent driven concurrency is not really easier than threads with primitive blocking synchronisation. Races are still possible, instead of deadlocks you can have livelocks, resource control and backpressure are non-trivial etc.
- anaphor 7y agoThat's true, but at least you can handle backpressure at the runtime level and just choose a predetermined strategy for dealing with it (i.e. start dropping messages, exponential backoff, etc)
- yongjik 7y agoI'm not exactly sure if events are easier to debug. I use tornado (event-based Python webserver library) extensively at work - when something goes wrong you don't get a nice stack trace, you get some random sampling of callback spaghetti. Also the default state of matters is that everything is serialized in a single thread and everything waits for their predecessor, even when they are totally unrelated, though that probably tells more about the particular framework than event-based programming in general. I'd rather use a real multithread-based framework, honestly, though I concede that it also opens up different ways of making developers' lives miserable.
- ailideex 7y agoThe programming paradigm provided by a threaded model (which is also there in async/await syntactic sugar) is a lot easier to follow and work with than asynchronous code paradigm IMO. I think writing code asynchronously is solving a problem all over your code base which can be solved much better by async/await or lightweight threads like golang has with goroutines and like jvm is getting with project loom - all of those solves the problem in the right place - allowing you to write code synchronously but execute it asynchronously. Once we de-couple these two things - how we write code and how we run code - the discussions about this become clearer and easier to have.
- maxmalysh 7y agoIt's time to migrate to async/await. Check out asyncio.Protocols for TCP servers and aiohttp for HTTP servers. We get beautiful async stack traces delivered to Sentry. Debugging anything is a joy.
- fjp 7y agoI’m always a +1 for aiohttp (and aiopg)’s ease of use
- rlpb 7y agoThe state of the art is improving. Trio for Python for example is much better in terms of stack traces. > Also the default state of matters is that everything is serialized in a single thread and everything waits for their predecessor, even when they are totally unrelated... I think this is a preferable default since bugs caused by pre-emptive multithreading are much harder to debug.
- milesvp 7y agoA shame that no one has mentioned cache invalidation as further reason threaded programming is hard. One my biggest takeaways from Martin Thompson’s talk on mechanical sympathy is that the first thing he tries when brought in as a performance consultant is to turn off threading. He mentions locking as a performance problem but that these days cache locality can be the key to speeding up slow applications.
- zzzcpan 7y agoYeah, it was hard to realize in 1995, but nowadays pretty much everyone who tried experienced performance problems with threads, or rather with shared memory multithreading concurrency model. It doesn't actually scale if you idiomatically synchronize shared memory access with locks or atomics, you need some way to batch things and amortize the cost of synchronization between cores while also preserving locality, which ultimately implies an asynchronous model where threads are just a low level implementation detail.
- milesvp 7y agoI've been hearing rumors that AMD's current offerings have been starting to avoid even shared cache between processors. It boggles my mind that any CPU designer would think a shared L2 cache is a good idea. Makes me wonder where my model of memory starts to break down. I always just think of L2 as being slower, less expensive memory than L1. I'm wondering if there are any benefits that actually outweigh the cache eviction penalty of multiple processors accessing it...
- kardos 7y agoIt's surely an engineering tradeoff resulting from weighing the different pros/cons. If you had dedicated cache per core at 1/Nth the size, much of it would be wasted when you're running less than full tilt -- eg, if you're using 2/4 cores then half of the cache is artificially unavailable instead of doubling the cache available to those 2 cores.
- gpderetta 7y ago
- bob1029 7y agoI haven't directly interacted with threads in a very long time, but I do use Microsoft's Task Parallel Library on a daily basis. I feel like once you understand TPL/async/await and the nuances with execution context and how to handle CPU vs IO bound operations, things come together really nicely. I do not really worry about things like cache coherency or the low-level synchronization primitives involved anymore. I can seamlessly throw in some locking with my TPL usage (typically ReaderWriterLockSlim) without much concern for strange behavior across the various tasks. It really does "just work" once you buy-in 100% (i.e. exclusively use TPL abstractions throughout). I would say that 99% of the time I am dealing with IO (simply awaiting some asynchronous database/network operation), with the other 1% of cases being things that I actually want to explicitly spread across multiple parallel execution units - I.e. Task.Run() or Parallel.ForEach(). In either case, I am working with the sugar-coated TPL experience, and all the horrific threading code is handled automagically. If I still had to work with threads directly, I would probably have found a different career path by this point.
- chrisseaton 7y agoWhat problems of threads is this actually isolating you from? It seems like the usual problems of correctly protected shared mutable state and avoiding deadlock are still there if you're using low-level primitives like ReaderWriterLockSlim.
- bob1029 7y agoI simply cite RWLS as an example of how you can combine other primitives into the threading model afforded by TPL without much frustration. I make no claims that it somehow eliminates fundamental concerns like shared state. In practice, I use RWLS extremely rarely because I prefer to avoid shared mutable state in the first place. I will spend an entire weekend reworking architecture in order to get a lock out of a hot path if I need to. 99% of my task-related code is boring stuff like: var session = await _connection.QueryFirstAsync<Session>(GetSessionSql, some session token); The biggest concern for me is simply the thread lifecycle and # of threads involved. TPL handles a thread pool for you and automatically schedules Tasks to run on these threads as appropriate. This is a non-trivial affair which would be painful to re-implement consistently and reliably in each application. I, for one, would be far too tempted to waste entire days screwing with thread pool parameters relative to environmental factors. With TPL hiding these things from me, I can focus on a level of abstraction that actually gets business features shipped. I still have not run into a scenario where I would have rather gotten my hands dirty and implemented the raw threads myself. Microsoft did a pretty damn good job. Everything "just works" and it scales very well.
- mmphosis 7y agoConcurrency isn’t a “nice layer over pthreads” - the most important thing is isolation - anything that mucks up isolation is a mistake. — Joe Armstrong
- drcode 7y agoPart of me thinks the success of javascript is due precisely for the reason that it prevents/discourages multithreaded programming.
- pedasmith 7y agoHey, I get to tell m y own two 1990's era threads story. First: in Windows 3.1, you got exactly one thread. My former company (BBN Software Products, home of the RS/1 statistical program) managed to get a version of RS/1 on Windows by splitting it into two pieces, each of which ran a single thread. On piece (RS/Client) was the UI; it talked to the "server" using TCP/IP (or a shared memory channel if the client and server were on the same machine) Second: I also got to help port a networking program over to an SGI box. At the time, the SGI GCC-based compiler could either supports threads, or support exceptions, but not both. (And my "unsupported" I mean, "generated code that would crash even if no exception was ever actually generated"). I couldn't convince the company to keep the threads and dump the exceptions, so instead I had to convert the program to spawn new processes with shared memory (!) to emulate the threads. TL/DR: actually programming with threads at the time was decidedly unsupported.
- projektfu 7y agoTrue, but Windows NT 3.1 came with threads and used them throughout the kernel. They were supported by nonstandard functions in the Microsoft C runtime as well as through the windows API. Windows included its own structured exception handling facility that also worked with it.
- marcosdumay 7y agoModern languages with high-level and verified async features and explicit mutability make threads much more convenient. That paper is finally becoming dated.
- cryptonector 7y agoI mean, threads are OK, but you need to essentially and artificially (because they're not processes) isolate them as much as possible (by minimizing shared state), and you want to do async/evented I/O so that you have as many threads as CPUs.
- _wldu 7y agoYears ago, an OpenBSD developer told me, "threads are for idiots" in response to a bug I had submitted. At the time, I was a bit offended (it was a good, reproducible bug report), but today, I think he was right. They're just too complicated and 90% of the time they are not needed.
- scottlamb 7y ago> Years ago, an OpenBSD developer told me, "threads are for idiots" in response to a bug I had submitted. At the time, I was a bit offended (it was a good, reproducible bug report), but today, I think he was right. Whether he was right or not about threads, it's offensive to insult the person just because you don't like their idea. Likewise for this to be the response to a bug report - if you don't want to support threads, then don't support them rather than lashing out at people who notice your support is buggy. I think in 2020 he's mostly wrong anyway. Sure, there have been many problems with threads, but... * this presentation's "Threads should be used only when true CPU concurrency is needed" maybe meant "rarely" in "1995" but means "commonly" in 2020 when single-core performance has been mostly stalled for a while and core counts have risen dramatically. * There are safer/easier alternate concurrency primitives than mutexes (channels) and at least partial solutions to major problems with threading. For example, in safe Rust there are no data races (even when synchronizing via mutexes). Other problems (deadlocks, contention, other types of race conditions) still exist of course. * "Threads" vs "events (event loops + callbacks)" as described in this 1995 presentation isn't the whole world, especially today. What about communicating sequential processes with no shared mutable state (such as Erlang's actors)? So to some extent I disagree with the framing of the problem altogether. * callbacks have their own problems beyond what's described in this presentation. Some that were widespread even in 1995: a string of operations written as a string of callbacks is a lot harder to understand than ones written with the "sequential composition operator" (;) and loops. (Callbacks are basically abandoning structured programming in favor of goto at the macro level.) Likewise harder to debug: you can't just get a stack trace and understand its current state. And some that have become more common since then. These days, event loops are usually multithreaded, so for any cross-request state you have the threading problems as well as the event loop problems. Today I'd say callbacks are the advanced, use them if you need the performance but be wary of the dangers option.
- bullen 7y agoEvents are not only an alternative to threads they are also a complement. If your language environment has a complex memory model with good concurrency support and stable non-blocking IO, you can use threads! But for them to be good you need to make sure that the input and output is compatible, as a general guide: 1) Make sure your hardware interface is capable of being accessed by the kernel from multiple threads. Network cards generally allow this, while graphics cards still don't, at least without a lot of overhead. 2) Make sure your application profits form parallelism, and specifically joint parallelism; which is my term for computing that allows many threads to work on the same memory. Bottom line is in my case only the Java server for my MMO will use threads in a "joint parallel" way. In everything else I will avoid them like a jerrycan full of gas tries to avoid fire. So I'm transitioning to C with arrays!
- jimjag 7y agoIt is amazing, and sad, how many people still refer to this document today when complaining about how "threads are bad" and "events are the bee's knees" or when justifying some architectural decision to avoid threads. No one prototypes anymore. No one does their own real world testing and benchmarking. They simply Google for some docs which support their already-made decision and call it a day.
- jlevers 7y agoThis is mildly off-topic, but how does one end up working on problems that are complex enough for things like this to even be an issue? It sounds incredibly interesting to me, but most of the software I've worked on has been at least somewhat web-based. I know there's so much more out there, and I'm just not sure how to find relevant problems to solve...it feels like a serious case of "I don't know what I don't know." I guess I should probably just pick some non-web concept I find interesting and start making something.
- yesenadam 7y agoPlease do that as an Ask HN, I'd be interested in the responses you hopefully get. Edit: Ah, I see that you did, an hour ago. Very good!
- InterestBazinga 7y agoI'm all for getting rid of threads, but what are you going to replace them with? Traditional functional languages may be the most obvious solution, but they're also among the most impractical of solutions. Is there anything else out there that can replace threading needs, without throwing out the book on programming? It seems like what we need hasn't been invented yet.
- kunglao 7y agoI've found C# to be one of the easier languages to do multithreading with. I think this is owing to the capabilities of a Visual Studio like parallel stack viewer. Also, anyone working with an OOP should really read Java Concurrency in Practice. That really helps in terms of learning how to think about multiple threads in a OOP world. Not sure how events by themselves can solve the threading issues as events can be multi-threaded too. I've seen people write far worse event driven code than multi-threaded code. If people want to use events heavily, I think it's better to use a well known design pattern so others can understand what you are trying to do.
- bodeadly 7y agoAsync is superior. I have done processes with locks in shared memory. I have done threads. But I predict Async will slowly start to take over. Processes are not suitable for working on shared data. Threads frequently yield race conditions and deadlocks even for experienced coders. But Async doesn't have any of these issues. So why isn't it more popular? For two reasons: 1) it completely breaks the functional programming model that we all learned as toddlers (instead of call A and then, after that's done, call B, Async is call A which just installs B as a callback, returns immediately and then an "event loop" calls B). Note that promises and tasks and futures are just "syntactic sugar". Personally I'm not a fan. I don't use any of that. I just use callbacks. 2) Even though Async it's great for concurrency, it's not great for parallelism. Everything runs with one thread. So if you want parallel processing you need workers. But I would argue that issue 1 can be overcome. In fact, I find Async to be quite elegant. I think in the long term people are going to realize that maybe we've had it backwards all along. Issue 2 is actually not that big of a deal for most things. It's actually somewhat unusual that you need to have some CPU intensive operation running in the background. Maybe image processing, data modelling, etc. But most blocking operations are just I/O operations which are not using CPU that much. If I needed to write some kind of network server, I would look at using libuv as a portable runtime.