Ask HN: Why does the C K&R Book not talk about threads?

20 points by agomez314 ↗ HN
I've been going over The C Programming Language book by K&R and noticed the glaring absence of threads in the book. Why is this?

38 comments

[ 4.4 ms ] story [ 84.9 ms ] thread
I can't answer your question, but I do want to say that when I read "K&R", my first thought was "Kidnapping & Ransom", and I was very confused and spent 30 seconds trying to figure out what the C stood for before finally clicking through... This is where my mental state currently exists, I guess.
What is your first thought when you look at Hermann Rorschach's inkblots?
Threads were not a mainstream thing back in the day. Even though 360 mainframes had threads in 1967, the original Unix didn’t have threads. Threads were important in Windows NT from the very beginning but Linux didn’t get them until 1996.
Windows NT needed to use the threads concept developed else where to make things workable because of way the OS shared address space.

https://en.wikipedia.org/wiki/Hybrid_kernel

Original linux/unix, same things could be done without the use of Windows NT style threads (aka fork/exec/pipe). Unix user space threads allow for a reduction in OS resource usage.

Threads concept: Outside of mainframe in 60's, threads just extra stuff unix service (80's internet), 1 running program providing a service to multiple users 90's forward - service on an internet port allowing multiple user access to a service, where extra unused resources per user add up fast!

More to the point, threads are an OS feature, not a language feature.

Atomic types needed to implement things like mutexes to serialize access to shared state have become a language feature, lately. But K&R was written before that happened.

The operations performed on those objects are Undefined Behavior as far as ISO Standards are concerned, but OSes have traditionally provided definitions, often by reference to Posix. So, a program using them is relying on definitions from both places, and usually several others besides.

Threads can also be a language feature which make them something of a leaky abstraction. For example, you can totally implement cooperative threading (aka virtual threads) through continuations and continuations can be implemented using only functions.

You're also sort of wrong even with regards to C. While threading isn't really cross platform, threads also can't be implemented purely as a library, see the paper "Threads Cannot Be Implemented as Libraries". You need at least some support from the language in terms of controlling operation reordering.

K&R certainly could have at least shown how to simulate concurrency in C. I wish I would have known about continuation passing/event loops when I implemented a dumbed down TCP on Arduinos.

Fork/exec & wait for "do your thing" notice is the pre-threads co-routine/continuation equivalent. Although, system("program to run") call allows better separation with less coding effort.

Don't think concurrency in C would have been included per unix philosopy of program does one thing well. Would have been program to feed data to named pipe, program to read data from named pipe.

Closest K&R C related example: Duff's Device & Simon Tatham's coroutines in C using macros. https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html

That page was a super-fascinating read, thank you!
If you don't have language support for atomic operations, you need to rely on assembly language library functions provided by the OS; or code your own. Those in turn rely on hardware features provided by the CPU manufacturer and usually bound to special machine instructions.

On simple-enough machines, it suffices to control when interrupts are enabled. More complicated machines with multiple out-of-order cores and fancy cache systems are enormously more fiddly.

A lot of concurrency can be implemented without atomics. Maybe in fact all? I'm not super deep in the theory.

But I don't think you can go far without controlling instruction reordering done by the compiler (as well as the CPU), which is what GP was talking about.

Not all machines reorder instructions, and not all machines have more than one core. You always need control over your compiler, but that often does not suffice.

With more than one core, you cannot do without one of atomics or custom assembly code sequences. And if the CPU reorders instructions all by itself, you need special instructions besides.

Atomics in the language mean you get to let the compiler worry about most things. But an atomic operation may take enormously longer than you hope. An atomic increment may take a hundred times as long as an ordinary increment.

To clarify, I take atomic aligned reads and writes of machine-sized integers (say 32bit) for granted. I'm not aware of any machines where these simple read and write operations aren't atomic. So I don't consider those "atomics". By "atomics" I understand something like atomic exchange, compare-and-swap, atomic inc/dec.

You can implement practical SRSW queues without atomics. Preventing reordering in compiler and CPU is enough.

I believe you don't need atomics to achieve mutual exclusion, at least not for 2 processes (Peterson's algorithm).

> You always need control over your compiler

Yes, this was the other poster's point, you cannot implement threading purely as a library. Function calls like pthread_mutex_lock() must be synchronization points and the compiler must not reorder any instructions around them.

Thread and/or concurrency abstraction (and/or other abstractions provided by a language) allows information/knowledge about how to deal with the abstraction (option or default) to be provided to the compile behind the scenes. aka when coding a program, when was the last time coded a stack/heap to do a function call with parameters instead of using a language provided way of calling a function with parameters?

This just seems weird with C because C language was designed to map as closely to hardware instructions as possible (hence why quite a lot of things in C are 'undefined' / dependent on platform used.)

Most modern languages are designed so that hardware issues are not a programmer's thing, but a compiler implementor's thing. Allows for much more sophisticated hardware without burdening the non-systems programmer.

Note that C is modern enough so all the thread stuff that you don't want to do yourself _can_ be done by a library, if you prefer. Why? Because the language already supports all the necessary primitives, like control over memory ordering.

I don't necessarily think that more is required in a language. Why would you want in a language what can be easily provided by a library?

This was first published in 1978.

We didn't have commoditized SMP machines until late 90s, early 2000s. Similarly, we didn't have 'good' threads until the early 2000s. In fact if you were coding in the 90s and early 00s you would've heard that threads were "bad", which was true at the time. Unfortunately, today you can still find this reiterated on places such as stack overflow even though it has long been untrue. We also deal with the fact that a lot of software made in the 90s has never been upgraded or replaced to deal with the architectural considerations of those days versus now. Today you can get up to 416 vCPUs on Google Cloud.

> SMP

It’s funny how more than a few programmers that I’ve talked to seemed to firmly associate threads (and processes) with the perceived need in multiple CPU cores to be able to run them. Preemptive multitasking on a single core simply did not look natural to them, to the point that some did not realize that it’s even possible. (One of them was also astonished to see, and to hear, a mechanical watch as he did not know such things existed.)

If you only have one core threading remains an expensive abstraction both computationally and mentally. Without at least two cores there’s no reason to prefer threading to coroutines, select(2), state machine loops, etc.
Yes, but having threading is important if you can then run that code on a super computer to get the benefits - I’m talking about yesteryear when you’re own computer might have a single core Pentium II or something, but you could then run it on an SGI or other with a lot of parallelism.
The K&R book is primarily focused on Unix and similar operating systems, and they didn't have threads when the book was written.

The first edition of K&R was published in 1978.

Wikipedia says that OS/360 had a notion of threads as early as 1967, although the distinction between threads and processes wasn't as clearly defined at that time.

In 1978, threading and concurrency was still an area of academic research. There are some great papers from Dijkstra and Lamport from that era, but much of the research at that time considered each concurrent "thread" to be a separate program; that is, each process was conceptually single-threaded, and if you wanted to do multiple things you made multiple processes for them.

The idea of multiple mini-processes inside a single process came later and took a while to stabilize into its modern form. POSIX didn't standardize thread-related APIs until 1995 and Linux didn't get modern POSIX threads support until Linux 2.6 in 2003.

POSIX threads date to the mid-1990s or so. Traditional C and UNIX simply did not have the concept of lightweight in-process threads, as used today. Any threading library would have been implementation-specific. In UNIX, if you needed another thread of execution, you started up another process.

Threads were certainly known of when C and UNIX were designed, but they weren't that common for typical programming tasks. Real-time tasks, mostly, where you need to respond to something while busy. Networks and GUIs were a big push behind threading becoming standard in most languages and environments in some way. But if you have no network sockets or other sources of events, and no need to render graphical output simultaneously with processing, you probably don't need threads. That's the kind of computing environment the K&R Book was written for.

Threads are not a feature of the C language.

In general, threads and their specific API are a feature of the OS.

Threads have been a C language feature for over a decade now, starting with C11. (although C11's threads had arguable posix comatibility issues until fixed in c17).
Yeah, but the latest edition of the K&R C book was 1988, predating C11 by a lot.
C language equivalent in late 70's would have been user defined array of functions (flyweight threads); use of fork()/exec() system call. (vs. current threads abstraction library/api)

Closest thing to direct "in-language thread concept" would be proposed C language definition addtion in which source code could specify a case statment block(s) be assigned to run on different cpu core(s) than the running program.

In 1987, I was a student intern at NASA Langley in a group that researched parallel computing. They had a dialect of C called Concurrent C that ran on a very specialized platform, the Encore Multimax, which IIRC had 16 Motorola 68000 CPUs. The language had special constructs for threading, synchronization, and message passing, which had to be supported by specialized bus protocols in hardware.

The point being, when K&R was written, threading was highly experimental, and it remained so for at least the next decade.

User level threads concept also doesn't really work with limited resources available for 60's/70's/80's machines.

Especially when charged by the precise amount of time/space/resources used, ?? 60's/70's mainfram vs. pc at home vs. today's cloud computing ??

The Multimax was NS16k/NS32k-based. After the Gould acquisition they switched to the 88K, but never (AFAIK) built anything on the 68K family.

Kind of a pity, really; nice hardware, but terrible luck with processor families.

You are correct. I dug out my Multimax manual, and my version says it came with dual processor cards populated with NS32032 CPUs operating at 12.5 MHz.
Are threads part of the C language or standard library?
It also doesn't seem to talk about libraries, which leads me to ask myself where the authoritative source is for reading about .dll, .dylib, and .so files. GCC and Clang documentation are about as close as you can get. To be fair, these are OS-specific details, but Unix-likes do have this, and the book talks about Unix system interfaces.

Mac OS X Archived documentation talks about it, presumably Microsoft documentation does as well, but AFAIK there is no official Linux documentation for it outside of compiler documentation.

K&R is C language specific

.dll, .dylib, .so are files created by/used by the linker the compiler calls to create/use the OS specific .dll, .dylib, .so files.

.dll is Microsoft Windows library

.so is unix related static library.

.dylib is MacOS related library.

"The Standard C Library" by P.J. Plauger. would be a match for stuff discussed in K&R.

Although, K&R is pre-C 89 standard, Plauger is more along the lines of C-89/92 standard.

More recent "standard ansi C library" : https://en.wikibooks.org/wiki/C_Programming/Standard_librari...

"Expert C Programming, Deep C Secrets" by Peter van der Linden can help with filling in the C 89/92 related miscellaneous stuff aka libraries, linkers/compilers, header files, make files, etc.
Because there are no threads in 1977 UNIX, and threads are not a part of C99 (but they are in C11).
Because they didn’t exist at the time that book was written. I took a Unix/c class as a comp sci elective (yes, you read that right). However this was 1988 so no Linux. We worked mostly on pc’s which was pretty progressive for the time and mainframes. We had a small mini computer (can’t remember from who.. dec? That doesn’t sound right..). Anyway, forked processes, no threads. In fact, I didn’t know what a thread was until the windows nt announcement in the early 90s. The irony is evident.