Hacker News new | ask | show | jobs
by nostrademons 2928 days ago
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.

3 comments

> the set of locks that a thread holds is basically an implicit dynamically-scoped global variable that can affect the correctness of the program

One technique to get a handle on this situation is making the mutexes actual explicit global variables.

"But global variables are bad" they will say. Yeah. And it reflects the reality.

"But I need a separate mutex for each object instance like they recommended in 1995 https://docs.oracle.com/javase/tutorial/essential/concurrenc... " they will say. Have fun with that.

Python and early Linux kernels use a single global mutex for access to all shared mutable state. In my experience, this is an entirely reasonable design decision for a huge majority of applications.

Well, you don't let lock semantics fall outside of a library frontiers. That means you do two things; first you do a global organization of the threads (no OOP-like patterns), second you export threads to the outside world in a hierarchy that exactly reflects the code hierarchy (easiest if you export a single thread).

There are some patterns that are safe as long as you implement them correctly. The patters that are good for IO are among the simplest, so that's where the GP was coming from. But it's not viable because he has full control of the code, it's viable because his problem domain has good options.

I agree with both of you. I don't think threads are THAT hard to work with. It definitely takes some experience to do it well and quite a bit more documentation to maintain the expected invariants. When libraries can get into a tangle, it's usually code that's in house and better ripped out. Easier said than done I know.

Open libraries tend to either just be single threaded abd should be used as such or explicitly thread-safe.

Disclaimer: Used threads in Java not much in C. Love me some Jsr-133 volatiles. Still confused with the Java 9 memory model updates.