The four calls that are the whole API
From the previous guide you carry one fact: a thread is a second runner inside one process, sharing its address space. Now we make one. The classic Unix interface is POSIX threads, pthreads, and for everyday work it boils down to surprisingly few calls. pthread_create() starts a new thread; pthread_join() waits for one to finish; pthread_exit() ends the current thread early; and pthread_self() hands back the calling thread's own id. Master pthread_create() and pthread_join() and you can already write real concurrent programs — the rest is detail.
Look closely at pthread_create(). Its signature is pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start)(void *), void *arg). That third parameter is the heart of it: start is a function pointer — the address of the function the new thread will begin running. A thread does not run a whole program from main(); it runs one function you name, top to bottom, and when that function returns, the thread is done. The function must have exactly the shape void *f(void *) — it takes a single void * and returns a void *. That rigid shape is how one API can launch any function you like.
Your first real thread, start to finish
Here is the smallest complete program worth running. main() is itself a thread — the main thread the runtime started for you — and it spawns a second one to run worker(). Then it calls pthread_join() to wait. Without that join, main() might return, the process would exit, and the brand-new worker would be killed before it ever printed a thing. Joining is not optional politeness; it is how the parent guarantees the child actually finished.
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void *worker(void *arg) {
long id = (long)arg; /* unpack the argument */
printf("worker %ld is running\n", id);
return (void *)(id * 10); /* this becomes the join result */
}
int main(void) {
pthread_t t;
int rc = pthread_create(&t, NULL, worker, (void *)7L);
if (rc != 0) {
fprintf(stderr, "pthread_create: %d\n", rc);
return 1;
}
void *ret;
rc = pthread_join(t, &ret); /* wait, and collect the return */
if (rc != 0) {
fprintf(stderr, "pthread_join: %d\n", rc);
return 1;
}
printf("worker returned %ld\n", (long)ret); /* prints 70 */
return 0;
}
/* build: gcc -O2 -Wall main.c -pthread && ./a.out */Trace the life of this program. main() runs pthread_create(); now two threads exist and the OS may run them in either order or genuinely side by side. The worker prints, computes, and returns a value. Meanwhile main() has parked itself inside pthread_join(), asleep, until the worker's function returns — at which point join wakes up, the thread is torn down, and its return value lands in ret. Two streams of control, one clean rendezvous. This is the join half of a pair you must always think about: every thread you create should be either joined or detached.
Passing in and getting back: it is all one void *
Notice the trick above: we cast a long to a void * to pass it in, and cast a void * back to a long to read the result. That works only because, on a typical 64-bit platform, a long fits inside a pointer. The honest, scalable way to pass real data — a struct, an array, several values — is to pass a pointer to it. You give the thread the address of something it can read, and that something must still be alive when the thread looks at it. This is where a sharp, common bug lives.
The classic trap is starting threads in a loop and handing each one the address of the loop variable: pthread_create(&t[i], NULL, worker, &i). Every thread receives the same address — the one and only i — and by the time the threads actually read it, the loop has raced ahead and i has changed. You meant to give thread 0 the value 0, but it may read 3. The fix is to give each thread its own live storage: pass an element of a per-thread array, or pass i by value through the cast trick. The deeper lesson is ownership and lifetime: the pointed-to data must outlive the thread's use of it, and on a thread's own stack a local dies when its function returns — so never hand another thread a pointer into a stack frame that is about to vanish.
Join or detach: every thread needs an ending
A finished thread does not simply evaporate. Like a process that becomes a zombie until its parent reaps it, a finished but unjoined thread keeps a little kernel and library bookkeeping alive so that someone can still call pthread_join() and read its result. If you never join and never detach, that bookkeeping leaks for every thread you make — harmless for a program that creates three threads, a slow death for a server that creates one per request and never collects them.
So there are exactly two honest endings, and join and detach are the names of both. Join it when you need its result or need to know it is done — pthread_join() blocks the caller until the thread finishes, then releases the bookkeeping. Detach it when you will never wait for it — pthread_detach() (or creating it detached from the start) tells the system to clean up automatically the instant the thread ends; a detached thread runs to completion on its own and you can never join it afterward. Pick one per thread. A thread that is neither joined nor detached is a resource leak with your name on it.
Where it breaks: two threads, one counter
Now we put the pieces together — and walk straight into the wall this rung exists to climb. Launch two threads that both increment the same global counter a million times each, join them, and print the total. You expect 2000000. You will often get less. Each thread runs a tidy loop of count++, but count lives in the shared address space — it is shared mutable state — and as guide 1 hinted, count++ is not one indivisible step but three: load, add, store. The scheduler is free to interleave the two threads' loads and stores, and when it does, increments quietly vanish.
- Both threads read count, which is currently 41 — each loads 41 into its own register.
- Thread A adds one (register = 42) and stores 42 back to count.
- Thread B, still holding its stale 41, adds one (register = 42) and stores 42 back too.
- Two increments happened, but count moved from 41 to 42 — one update was silently swallowed.
Run that program five times and you may see 2000000, 1998412, 1999999, a different wrong number each time — the answer depends on timing that changes every run. This is a race condition, and it is not a flaw in pthreads or in your loop; it is the unavoidable consequence of two threads touching one mutable variable without coordination. The function each thread runs is not thread-safe — it cannot be safely run by two threads at once on shared data. Crucially, creating the threads, the whole subject of this guide, is the easy and reliable part. Coordinating them is the hard part, and it is where the next two guides go.
What you can now do, and the cliff ahead
Step back and own what you have. You can now spawn a real OS thread with pthread_create(), aim it at a void *(*)(void *) function, smuggle an argument in and a result out through a void * (a value via a cast, real data via a pointer to something that stays alive), check every return value, and end every thread cleanly with join or detach. That is the full, honest mechanics of getting concurrent code to run. It is genuinely not much API — and that minimalism is the point: pthreads gives you the runners and gets out of the way.
But that last example is the warning shot. The threads ran perfectly; the answer was wrong, sometimes, unpredictably. Nothing crashed, nothing printed an error — the program just lied about a number, on some runs and not others. That is the uncomfortable face of concurrency: the hard part was never starting the threads, it was that they share memory and the sharing is uncoordinated. Guide 4, The Race Condition: When count++ Lies, takes that broken counter fully apart, instruction by instruction, until you can predict exactly how and when an update gets lost. Guide 5, Why We Need Synchronization, then hands you the cure — the mutual exclusion that lets only one thread into the dangerous load-add-store at a time. You have built the engine; next you learn why it needs a lock.