back
97 comments
Oh gee I guess I'm a wizard.

Lots of systems/embedded programmers roll their eyes at this kind of talk. Threads aren't really that hard.

Event queues do have benefits in certain situations. They pair nicely with state machines. You can easily end up in callback hell though, and it is often difficult to integrate some long-running, atomic tasks into your event loop. You end up doing things like having a thread pool, at which point you have to wonder why you stopped using threads in the first place. Oftentimes a threaded approach is a cleaner approach. Just get the locking granularity right - it's not that difficult.

Systems/embedded programmers roll their eyes at this kind of talk because they usually control (or at least have visibility into) all of the code that goes into their stack. Threads aren't that hard under these conditions.

The main problem with threads is that they're non-composable: the set of locks that a thread holds is basically an implicit dynamically-scoped global variable that can affect the correctness of the program. If you call into an opaque third-party library, you have no idea what locks it may take. If it then invokes a callback into your own code, and you then call back into the library, there is a good chance that your callback will block on some lock that a framework thread holds, that framework thread will block on a lock you hold, and then the code that releases that lock will never execute. Deadlock.

If you control all of the code in your project, this does not affect you: define an order in which locks must be acquired and released and stick to it. If all of your dependencies have no shared data and never acquire locks themselves, this does not affect you (and indeed, this is recommended best practice for reusable libraries). If you never call back into third-party libraries from callbacks, this does not affect you, but it severely limits the set of programs you can write. If all of your dependencies thoroughly document the locks they take and in which order, this affects you but you can at least work around the problem areas and avoid surprise deadlocks.

Most application developers do not work under conditions where any of these are true, let alone all of them. Application development today largely consists of cobbling together third-party libraries and frameworks, many of which are undocumented, many of which are thread-unsafe, and many of which spawn their own threads and invoke callbacks on an arbitrary thread.

Quite. I'm so fed up of the "threads are bad" argument (in my mind it's been commonplace since about 2008, so it's interesting to see this piece from 1995).

I've made use of threads at some point in almost every single job of any duration. They're one of many problem solving tools and if you understand them, which isn't particularly difficult, at some point you're bound to run into a problem that's a natural fit for a multi-threaded solution.

Nowadays, especially with no shared state, they're super-easy to use on many platforms. Take, for example, the parallel support in the .NET framework, along with functionality that supports debugging multi-threaded apps in Visual Studio like the ability to freeze threads.

If you do need to share state, which is when locking becomes essential, most languages and platforms have easy to use constructs to help you do this without much in the way of drama.

I'm not suggesting for a minute that there are no dangers, but there are plenty of dangers with other programming techniques, as well as lurking in any system of sufficient complexity, so I don't really understand why threads garner so much hate.

A lot of work in programming languages over the past decade has been devoted to providing a safety net and guard rails for avoiding the pitfalls of thread-based concurrency. See in particular Rust and Go. It's still quite possible to corrupt data and get deadlocks, but our languages have come a long way to making it harder.

But the point of this article is to say if we ditch the notion of threads entirely and go with this other thing, we won't need safety nets anymore because it will be impossible to deadlock and corrupt data (as opposed to less likely).

Structured threads aren't that hard (e.g. task-based systems, thread pools).

Unmaintanable raw-pthread messes are a nightmare sequel from the director of Endless GOTOs.

Yes, small careful software teams can make threads work. However, if you start to work with physicists, mathematicians, electrical engineers, and so on who are incredibly smart in their own areas, but who don't have or even value a skill in software, you'll discover they make a real mess out of threaded programs in a way that doesn't happen with separate single-threaded processes.
There's even a course from GA Tech (Intro to Operating Systems, publicly available on Udacity) that covers how to use threads safely and sanely. I went in knowing nothing but terror from a failed experiment in naive multithreading and came out wanting to apply threads to everything. Maybe not quite the right approach, but I at least feel vastly more confident with keeping them manageable. Like you say, managing how and when to lock is the key.
If your state machine event handlers are non-blocking, then the thread pool is the same size as the number of available hyperthreads. That's not hard either. And, as observed elsewhere, it becomes impossible to screw up. That's a powerful property, and makes it possible for non-embedded 'normal' folks to write correct code in this space.
There is a response to this: Why Events Are A Bad Idea (for high-concurrency servers)[0]

[0]: https://people.eecs.berkeley.edu/~brewer/papers/threads-hoto...

Oh, to return to what life was like 23 years ago, when GUI applications were so simple that you could get away with fitting all their work onto a single thread.

Nowadays, a great many GUI apps have a lot of data crunching to do in the background. You've really got two options for how to handle that:

  1. Be intermittently unresponsive, like iTunes.

  2. Do work on a background thread, like decent software.
(Intentionally omitting the option of breaking your work into a bunch of tiny bits that can be handled on a single thread's event queue like some sort of deranged node.js app from hell, on the grounds that please no I can't even.)
3. Work on a task handled by a thread pool, like scalable software

4. Work on another process, like safe software

Even 20 years ago single threading was well known to be inadequate – anyone doing file or network I/O got daily reminders of how bad that was as the UI wedged, not to mention MacOS where holding a menu open froze an app.

Windows NT, OS/2, and BeOS were better, if only the hardware cost cane down and we could ditch this legacy DOS/Win16 code…

The source of the article, Sun, is interesting.

I guess the author knew what was about to be foisted upon the world.

Kudos for trying to warn us.

(I remember reading Novell and OS/2 documentation in the late 80s / early 90s about threads and recoiling in horror. Of course, all real men must use threads, cuz they’re faster, even if stupefyingly dangerous)

The author is John Osterhout, a CS professor. He was working at Sunlabs at the time. It's not like some unknown lone voice from the bowels of Sun was 'trying to warn us'. Warn us about what, anyway?
John Ousterhout is the inventor of the Tcl language.
A serious practical problem with threads mirrors the same problem with C++, which is that many programmers reach for it first when they should be reaching for it last. Both of these technologies are like swallowing glass, and the wise programmer will avoid them if at all possible.
It depends on your field. If your area of expertise is high performance software, C++ can be the right choice for most of your problems.
> that many programmers reach for it first when they should be reaching for it last.

What's the go-to solution to get a UI to not block when running a computationally expensive task that takes a lot of time to finish?

I have seen that argument in the past but somehow still can't see how event-based approach saves us from the headaches of concurrency.

If you need to deal with parallel processing (which is relatively often in the real world) you WILL have to face the problems of consistency, visibility and program order.

Many languages don't even require programmers to have much exposure to threading mechanics. It's an OS responsibility, and that's not necessarily a bad thing.

1995 was a different era.
> Only use threads where true CPU concurrency is needed.

This is the case much more often now than it was in 1995.

Not just "more often" but essential. We've been at four hardware threads for a decade now, and we're just entering a new era where increasing the number of CPU cores is becoming a marketing tactic.
CPU concurrency is not incompatible with events.

Shared mutable state is where the problem lies, but in a lot of cases shared memory can be used to pass it without copying from a "scheduler" thread to worker threads for parallel processing in non-overlapping chunks, and then back for compiling into a whole.

Somebody better tell the Node.js cluster guys :-)

They manage without threads pretty well, I think. Shared state is deliberate, outside of individual processes, rather than accidental in-process. As it should be unless you are doing some serious systems level programming.

Under Linux, you don't need threads. Threads and processes are basically the same thing. You just provide different flags which tell the kernel how it should view that process, which will impact things like memory isolation, copy on write, etc.

Under Windows, it is a different story. Threads and processes are wildly different constructs, and threads are more lightweight. Sometimes, still not lightweight enough, so they came up with fibers.

Linux is the exception on how most UNIXes implement threads.
Threads are not hard. In fact, threads are extremely easy to implement.

However, real Threading code is just incredibly difficult to reason, just by looking at it. This makes it easy for you to introduce race conditions without even knowing that there is one!

There is also the fact that locks don't lock anything! They are just a flag, that a any code may choose to ignore.

They are a not an enforcing tool, just a cooperative one.

(More here: https://www.youtube.com/watch?v=9zinZmE3Ogk)

P.S. I created a library, that makes it easier to write safer multiprocessing code

https://github.com/pycampers/zproc

I've built things with pthreads a few times, and also used threading in Java, Rust, Python, and Ruby. (Edit: C# and Perl too IIRC. :-) The best book I've read about using threading safely was the O'Reilly Java Threads book. It's been about 16 years, but I remember it being a great "teaching the concepts" book, taking you through lots of pitfalls and showing how many ways you can mess up. It taught me way more than just Java. Like oldgeezr I kind of roll my eyes at the "you must be this tall to use threading" stuff, but I think I largely owe to that book both my confidence and my wariness. I bet it is still worth reading today.
Java Concurrency In Practise served the same purpose for me. A very good book indeed.
Also, I have bad news for you if you are a web developer: unless all you're serving up is HTML (either static or generated on the server[1]) you are developing a multi-threaded app - it just happens to be the case that the threads are running on different machines. Race conditions between client and server code are a thing, so you'd do well to understand the concepts of multi-threading.

[1] And even in this case you're probably still multi-threaded, although in most cases it won't feel like it because your server side threads won't share state.

Threads were overprescribed in the 90s. I’m pretty sure the original example code for drawing an image in Java involved typing “new Thread” so that the image could be loaded over the network in the background. Truly the way to write apps in the Internet age!

At the same time, Java’s threads are so easy to use — without of the portability or debuggability issues of native threads — that threads don’t seem that bad to Java programmers. Yeah, shared state can be a foot gun, but so can global variables. You just keep things as pure and easy to reason about as possible. And Java has had concurrency primitives that keep you from having to deal directly with threads and locks for over a decade.

I don’t think “events” and threads solve the same problem. If your program would work just as well doing all its work in a single thread then yeah, you don’t really need threads. If we’re comparing “events and callbacks” async style to async/await style where you write your code as if it were running in a thread (even if it isn’t), I think the latter wins.

Why Ideas are Bad Ideas (1995-2018)
As mentioned in other comments, Ousterhout is the inventor of Tcl/Tk (among other things). At around the time this was published, Tcl was my favorite "play with" language, and it naturally lacked sort of built-in threads abstraction. Also at the same time, Tcl/Tk had just become a project at Sunlabs. One of their early projects was to take the event-loop that was the underpinning of Tk and add it to Tcl.

I started a new project back around then to build a system for deep caching of web sites to give time consistent access offline. I implemented as a web proxy with an online/offline button. As you browsed web sites, it would crawl recursively following a set of rules. The intent was to precache content near what you already explicitly accessed, to make it available offline later on (we called this the "detachable web").

While not the primary purpose of our project, I put together a demo to optimize the Alta Vista search results page, which at the bottom only had a "Next" button (unlike the "1 2 3 4 5..." you see at places like Google today). When you clicked "Next", it took Alta Vista a few seconds (4-5) to return the next page of search results. My system would prefetch the 10 pages of results by POSTing the "Next" for you, basically while you were still reading the first page resuls. The result was "Next" became instantaneous. Again, this is not why we built this system; this was just one novel approach I used it for.

I mentioned all this because the entire project was implemented in Tcl. Being influenced by the lack of thread support in Tcl and by the paper mentioned in the OP, my project utilized a event-driven model for everything, since every inbound user require could fire off dozens of background fetches, all of which needed to be done in parallel. Events (and continuations) worked well for this. I have a paper up from the 5th Tcl/Tk workshop:

https://www.usenix.org/conference/5th-annual-tcltk-workshop-...

I had used for the project Tcl because it let me support all three prevalent platforms of the time: UNIX, Windows 95, and MacOS 9. Day-to-day work was done on FreeBSD.

I think have some commentary in the paper on the effects of the event-driven approach. What's funny is that I was taken off the project for v2, which the team then decided would be written in Java using threads, because, well, Tcl wasn't mainstream enough. In 1997, Java was the rage. The downside is that they could never get v2 working reliably enough because of the explosion in memory and processing power it required to accomplish the same work. In Tcl, having 60 traversals active when it was just 60 continuations (events) just worked. In contrast, the Java implementation needed 2-3 threads per traversal, and it just couldn't scale up to that.

Nice story. Interesting notes in your paper on the lack of a standard library. In the end, I think this is what have killed the language.
Like in almost any functionality of a computer or programming language, it helps to understand them, being aware of the risks and knowing alternative approaches.

Threads can be a bad idea but if you keep in mind what variables you use and guard shared memory, it's fine. Sometimes you might prefer a process instead for security/resistance.

Just FMI: the "events" approach that's recommended in the article over threads, that's how Python libraries like tornado and twisted work, right? And to what extent does the new asyncio Python library assume that functionality?
Events don't share mutable state (at least implicitly), they carry a copy of data. This eliminates a huge class of thread-related errors.

The canonical thing that works this way is Erlang (and its modern cousin, Elixir). See also "actor model" (e.g. Akka). It is approximately how Windows and Mac GUI used to work (back in the day; did not look at these APIs for ~20 years).

Python async is coroutines, a different kind of concurrency. In it, the event loop is hidden, and coroutines just yield control, implicitly or explicitly, to allow other coroutines proceed. In Python, a CPU-bound task can only run on a single thread, due to the Global Interpreter Lock preventing concurrent modification of data. Coroutines are still useful both for IO and as a general way to describe intertwined, mutually dependent computations. (The earliest, limited Python coroutines were generators.)

I was not coding in 95, and therefore don’t understand the perspective of the author back then, but it seems clear from the presentation that the culprit was “shared mutable state”, not “threads”. Wasn’t functional programming a thing back then?
Is this still true today? Even back when I was a young coder threads really weren't that difficult for me to understand and develop. With many of the modern languages, threading is even easier than before.
Oh look, an ad for Visual Basic from 1995! Event driven code sucks and is not better. I learned this writing VB code. Others learned the hazards of event driven code in the Therac 25.
Use Clojure, it has built-in Software Transactional Memory support.
Must be real wizards these days boys. Today we deal with hundreds of instances of an application each running many threads across multiple cpu cores.
Still are. But goroutines are o.k. :)
To me, serious work in an event loop feels like a more convoluted form of cooperative multitasking.
OK threads aren't "BAD" or "GOOD", threads are a tool to be used correctly.

Threads are like a data super-highway and all the incorrect uses of them arise from using them for way to little data. Akin to building a 5 lane highway for 5 cars to pass.

A thread has some amazing things of being able to switch an execution very fast (built into things on the CPU level) and memory caching/storing advantages. Aka a thread is meant for a compute heavy task like rendering something, or running a decode in the background, mainly doing heavy math. Threads provide great things but at a cost. just like a highway they cost a lot ( a lot of memory in your ram) and require some maintenance and management (locking mechanisms)

The problems with threads arise when people think its ok to use them everywhere for all tasks parallel or async.

Example Apache used to start a thread for each connection to server which at that time took 40 MB + .5 sec and this allowed a myriad of attacks on, one of them being slow loris.

In java-script if you start a new web worker thread, that's actually a new V8 instance and costs you again a lot in memory and startup time.

This "start a thread for everything" was definitely the prevelant thinking in the first decade of 2000, and people were not really thinking about hidden costs.

Come along Ryan Dahl with node.js in 2009 and "OMG everyone forgot there are such things as event loops"

An event loop is basically a much cheaper single threaded async way of processing events in an event queue, the big idea here was that in most other languages threads waited for any time consuming I/O to network or hard disk and let other threads run in the meantime.

Ryan combined the async nature of event loops with async I/O... rightfully a very clever move. (also I/O locks is what often causes thread locks in multi-threaded environments)

This allowed the single threaded event loop to never really lock up with any time consuming, but not CPU related task, freeing the CPU to constantly process the event queue, in a way emulating multi-threading on a single thread.

Going back to the highway metaphor, this would be more like an elevated city bike path, it cant take heavy trucks (heavy CPU loads) but it can take a huge amount of light processing request and never lock up, freeing up your city streets from bikers and leaving them more free to run the heavy trucks.

This is how node js can handle 600k concurent connections - https://blog.jayway.com/2015/04/13/600k-concurrent-websocket...

something you would never be able to achieve if u started a thread for each one.

basically this is akin to building 1 dense bike path for 600k bikers or building 600k 5 lane highways down each only 1 biker would go.

Where node.js falls short is if u give it heavy math tasks, the event loop will lock up.

So in my analytics processing server i had a node.js main loop with a bunch of V8 web-worker thread pools, to do the heavy math and statistics, while the main thread just routed requests and served cached data.

Another consideration however is memory leaks, threaded environments tend to clean up well after themselves, because if there is a leak in a thread it gets wiped when the thread dies. But node.js is very susceptible to memory leaks.

All these things are just tools, you have to learn when to use the right tool for the right job.

But i think there are much more pitfalls in building threaded environments then there are using event loops. I got node js concepts within a week or two, however i still struggle with some thread lock concepts even after taking clases, and shit is way harder to debug properly too. Its that high abstract level of thinking that i have a hard time visualizing in my head, and i am never sure that i though EVERY scenario through.

Yep, they might be a good solution on resource constrained hardware, but we learned hard how bad they are from security and stability point of view.

In that regard process are much better solution.