This episode opens up multithreading with pthreads, synchronization primitives such as mutex, semaphore, and condition variables, the dangers of shared memory, race conditions, and how to avoid deadlocks, as well as an introduction to OpenMP for parallel loops.

Modern processors have many cores, and a program that uses only one wastes most of its power. Episode 16 discusses concurrency: how to run many tasks at once using threads, and how to coordinate them without conflicts.
The POSIX Threads library, or pthreads, is the standard way to write multithreaded programs in C. With it comes great responsibility: threads sharing data without synchronization produce race conditions that trigger the hardest bugs to track down.
The second half of this episode introduces OpenMP, which turns loops parallel with just one directive — a quick way to gain speed without writing manual thread management.
Threads are created with pthread_create and awaited with pthread_join. Each thread runs a function that receives an argument:
cat > thread.c <<'EOF'
#include <pthread.h>
#include <stdio.h>
void *tugas(void *arg) {
int id = *(int *)arg;
printf("thread %d berjalan\n", id);
return NULL;
}
int main(void) {
pthread_t t;
int id = 1;
pthread_create(&t, NULL, tugas, &id);
pthread_join(t, NULL);
return 0;
}
EOF
gcc -Wall -Wextra -pthread thread.c -o thread && ./threadpthread_create(&t, NULL, tugas, &id) starts a new thread that runs the tugas function with the &id argument. The -pthread flag at compile time is mandatory — it links the thread library and defines the needed macros. pthread_join waits for the thread to finish before the program ends.
Threads within one process share the same memory. Global variables can be read and written by all threads. This convenience is the source of the biggest problem: two threads writing the same variable simultaneously produce unpredictable results.
A mutex ensures only one thread enters a critical section at a time:
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t kunci = PTHREAD_MUTEX_INITIALIZER;
long counter = 0;
void *naikkan(void *arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&kunci);
counter++;
pthread_mutex_unlock(&kunci);
}
return NULL;
}The pattern pthread_mutex_lock(&kunci); ...; pthread_mutex_unlock(&kunci) locks the critical section. Without a mutex, two threads incrementing counter at the same time can lose updates. Keep locks as short as possible: locks held too long leave other threads idle, and inconsistent locking causes race conditions.
A semaphore counts available resources, for example limiting the number of active connections. A condition variable lets threads wait until some condition is met: one thread signals with pthread_cond_signal, others wait at pthread_cond_wait. This pattern is used for worker queues — threads wait until new work arrives, then process it.
A race condition occurs when the result depends on an uncontrolled execution order between threads. Detection uses ThreadSanitizer during development:
gcc -g -fsanitize=thread -pthread program.c -o program
./programThe -fsanitize=thread flag injects checks that report every data race in detail. Like AddressSanitizer in episode 11, this tool finds what the eye can't see and is only active during development.
A deadlock occurs when two threads each wait for a lock held by the other. Effective prevention rules: always acquire locks in the same order across the whole program, and avoid holding one lock while waiting for another. If possible, use one mutex per unit of data and document the locking order.
OpenMP parallelizes loops with a simple pragma. Build with the -fopenmp flag:
#include <omp.h>
#include <stdio.h>
int main(void) {
long total = 0;
#pragma omp parallel for reduction(+:total)
for (int i = 1; i <= 1000000; i++) {
total += i;
}
printf("total: %ld\n", total);
return 0;
}
EOF
gcc -Wall -Wextra -fopenmp omp.c -o omp && ./ompThe directive #pragma omp parallel for reduction(+:total) divides loop iterations across available threads, and the reduction sums partial results without a race condition. OpenMP handles thread creation, work distribution, and result merging automatically.
OpenMP adds speed only if the loop is safe to parallelize: no dependencies between iterations. For iterations that depend on the result of a previous iteration, parallelization will be wrong. Measure speedup with omp_get_wtime and compare serial and parallel versions.
Warning
Concurrency is the domain with the hardest bugs. Start from correct serial code, then add threads with minimal synchronization, and always test with ThreadSanitizer. Speed means nothing if the results are wrong.
Key takeaways:
In the next episode 17 we will discuss advanced memory techniques — memory alignment, padding, and pointer arithmetic, custom allocators and a basic memory pool, memory sanitizer and leak detection, up to writing reliable low-level code for embedded and real-time.