1. THE DEEPER STORY (IS A LIVING MACHINE)
Imagine a quiet workshop.
One worker enters.
His name is main thread.
He has one notebook (stack), one pair of hands (registers), one path to walk (instruction pointer).
He reads instructions one by one from the program text section.
Everything is calm. Deterministic. Linear.
Then work increases.
The workshop manager says:
“We need more workers.”
And we call:
pthread_create(...)
CHAPTER 1 — BIRTH OF A THREAD
pthread_create is not magic.
It is a negotiation between your user-space library and the kernel.
What the pthread library does first
Before asking kernel, libc/pthread prepares:
- thread metadata (Thread Control Block in user-space bookkeeping)
- stack memory for the new thread
- thread attributes (stack size, detach state, scheduling hints, etc.)
Then kernel is asked
Under Linux (NPTL), this is usually built on top of clone-style thread creation.
Kernel creates a schedulable entity with:
- new register set
- new stack pointer
- entry point = your routine function
- same process virtual address space (
mm_struct) as other threads
Now your process has multiple execution streams.
CHAPTER 2 — WHAT IS SHARED, WHAT IS PRIVATE
All threads in one process share:
- heap
- global/static variables
- code/text
- file descriptor table
- process ID (with per-thread TID identity)
Each thread has private:
- stack
- CPU registers
- thread-local errno / TLS (Thread Local Storage)
- scheduling state
Think of it like:
Same house, different rooms.
If two people edit the same whiteboard at once, chaos starts.
CHAPTER 3 — THE SCHEDULER, THE ILLUSION OF “AT THE SAME TIME
You think threads run together.
Sometimes true (multi-core), sometimes illusion (single core timeslicing).
Scheduler decides:
- who runs now
- for how long
- who gets preempted
- who sleeps
Context switch is expensive tiny surgery:
- save CPU state of thread A
- load CPU state of thread B
- switch stack/register context
- continue B exactly where it stopped
With many threads, this happens constantly.
Too many threads = too much switching = performance loss.
CHAPTER 4 — WHY RACE CONDITIONS ARE SNEAKY
You write:
counter++;
Looks like one operation.
At machine level it is many steps (load/add/store).
Thread A and B can interleave:
- A loads 5
- B loads 5
- A add 1
- B add 1
- A stores 6
- B stores 6
but we need 7!
One increment vanished.
No crash. No warning. Just wrong answer.
That is why races are dangerous:
bugs that look legal but are logically broken.
CHAPTER 5 — MUTEX: THE DOOR WITH ONE KEY
-
pthread_mutex_tis the key system. -
pthread_mutex_init: create lock object -
pthread_mutex_lock: try entering critical room -
pthread_mutex_unlock: leave room -
pthread_mutex_destroy: cleanup
If lock is busy:
- thread blocks (usually via futex path in Linux)
- thread sleeps in kernel wait queue
- no busy CPU spinning (in normal contended path)
Important deep point:
Mutex also gives memory ordering guarantees.
It is not only one at a time, it is also what happened before unlock is visible after lock.
So lock/unlock is both:
- exclusion
- synchronization barrier
CHAPTER 6 — CONDITION VARIABLE: “DON’T POLL, SLEEP UNTIL EVENT”
Mutex alone says “protect this region”.
But what if thread must wait for a condition (queue_not_empty)?
You could loop and check forever (busy wait). Bad.
Instead:
pthread_cond_wait(&cond, &mutex);
Internally:
- thread is in critical section with mutex held
- wait call atomically:
- enqueues thread on condition wait queue
- releases mutex
- sleeps
- on signal/broadcast, thread wakes
- thread re-locks mutex before returning from wait
That atomic release+sleep is crucial.
Without it, signals can be missed (lost wakeup).
Why while, not if
Because wakeups can be:
- spurious
- or condition changed by another thread before you reacquire mutex
So always:
while (!condition)
pthread_cond_wait(...);
CHAPTER 7 — JOIN, DETACH, AND THREAD DEATH
When thread routine returns (or calls pthread_exit), thread finishes.
But resources may remain until joined (joinable threads).
pthread_join:
- caller blocks
- target thread termination is collected
- stack/resources reclaimed
If detached:
- no join allowed
- resources auto-reclaimed at exit
Forgetting joins on joinable threads is like leaking zombies in user-space lifecycle.
CHAPTER 8 — TIME: WALL CLOCK VS MONOTONIC REALITY
Many projects use gettimeofday.
It gives wall-clock time (can jump due to NTP/time adjustments).
For intervals/timeouts, monotonic clocks are safer (clock_gettime(CLOCK_MONOTONIC)).
usleep:
- asks kernel to park thread for minimum duration
- wakeup is not exact; scheduler latency may delay return
- “sleep 1000us” means “at least around that, maybe more”
So real concurrent timing is always approximate, never perfectly exact.
*CHAPTER 9 — WHAT REALLY HAPPENS IN YOUR SIMULATION (LIKE CODEXION)
*
Your coders are threads.
Dongles are shared resources.
Monitor is watchdog thread.
Global condition is wake-up bell.
Flow:
- coder tries to get two dongles
- if impossible, sleeps on condition
- another coder releases dongles + broadcasts
- sleepers wake and race to re-check condition
- one succeeds, others sleep again
- monitor checks burnout deadlines
- if death condition true, global stop flips
- broadcast wakes all blocked threads so they can exit cleanly
This is a tiny operating system inside your app.
2. Practical + Debugging + OS internals
pthread_create
Function:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
1) The story in one setence
pthread_create asks the kernel to create a new schedulable execution context
(a “thread”) inside the same process address space, with its own stack and
registers, then starts running start_routine(arg).
1) Practical mental model (42-project level)
After pthread_create succeeds:
- You now have TWO flows executing concurrently.
- They share:
- heap (malloc/free)
- global/static variables
- file descriptors (open files, sockets)
- They do NOT share:
- stack memory (each thread has its own stack)
- CPU registers
- instruction pointer (where it is currently executing)
Implication:
- Any shared variable must have a protection policy (mutex/atomic/cond protocol).
- Local variables in a thread routine are on that thread’s stack; other threads cannot see them unless you pass pointers to shared memory.
2) OS internals (Linux, conceptually)
On Linux with NPTL, a “pthread” is usually implemented as a kernel thread
created via clone-like mechanisms.
Kernel sets up:
- a task_struct (scheduling entity)
- a new stack pointer
- CPU register state for initial start
- thread ID (TID), while still sharing process address space (same mm_struct) Scheduling:
- The kernel scheduler sees threads as runnable tasks.
- On multi-core: truly parallel.
- On single-core: time slicing and context switching. Context switch (high-level):
- Save registers of thread A
- Load registers of thread B
- Switch stack pointer
- Continue B Cost:
- Not free. Too many threads => too many context switches => slowdown
3) Debugging + common mistakes
If main returns before other threads finish:
- process ends => all threads die immediately. Fix:
- pthread_join, or detach threads.
pthread_create can fail (e.g., resource limits).
Always:
- check return value
- print error, clean up, exit.
5) Mini exercise (focused on pthread_create)
Goal:
- Create N=5 threads.
- Each prints "hello from thread X".
- Ensure every thread prints a unique id.
Constraints:
- Must not pass &i.
- Must compile with -Wall -Wextra -Werror -pthread.
# include <pthread.h>
# include <stdio.h>
# define N 5
void *task(void *data)
{
int id;
id = *(int *)data;
printf("hello from thread %d\n", id);
return (NULL);
}
int main(void)
{
pthread_t thread[N];
int ids[N];
int i;
i = 0;
while (i < 5)
{
ids[i] = i;
pthread_create(&thread[i], NULL, task, &ids[i]);
i++;
}
i = 0;
while (i < 5)
{
pthread_join(thread[i], NULL);
i++;
}
return (0);
}
Top comments (0)