Understanding Threads and Their Advantages
In Unix-like systems, threads are often described as lightweight processes. While threads are not processes themselves, they can be considered cousins of Unix processes. Multiple threads within the same process share the following system resources:
- Virtual address space
- File descriptors
- Signal handlers
- Other process-level resources
However, each thread maintains its own:
- Call stack
- Register context (execution environment)
- Thread-local storage
Threads enable applications to improve performance in multi-core environments, particularly when handling operations that might block, such as file I/O or socket communications.
When comparing multi-threading to multi-processing, the communication overhead differs significantly. In multi-process models, data exchange requires context switches between user space and kernel space. Multi-threading eliminates this overhead since threads can communicate through shared global variables.
Thread Lifecycle: Creation, Termination, and Joining
Creating Threads with pthread_create
The pthread_create function accepts four parameters:
- Thread identifier variable (output)
- Thread attributes pointer (NULL for defaults)
- Function pointer for the thread routine
- Argument to pass to the thread function
pthread_t thrd1;
pthread_attr_t *attr = NULL;
void *thread_function(void *argument);
char *some_argument = "Hello";
pthread_create(&thrd1, attr, thread_function, some_argument);
Terminating Threads with pthread_exit
pthread_exit(void *retval);
The retval parameter stores the thread's exit status that other threads can retrieve.
Waiting for Threads with pthread_join
After pthread_create succeeds, the execution order of the new thread and the calling thread is determined by the operating system's scheduler. When you need to wait for a specific thread to complete, use pthread_join—analogous to waitpid in process programming.
When thread A calls pthread_join on thread B, thread A blocks until thread B terminates. Upon B's exit, thread A collects B's return status.
Function parameters:
pthread_t th— identifier of the thread to wait forvoid **thread_return— location to store the terminated thread's exit status
pthread_join(thrd1, NULL);
Complete Working Example
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *worker_routine(void *arg);
int main(void)
{
int status1, status2;
void *exit_status;
pthread_t tid1, tid2;
const char *msg1 = "Worker-A";
const char *msg2 = "Worker-B";
status1 = pthread_create(&tid1, NULL, worker_routine, (void *)msg1);
status2 = pthread_create(&tid2, NULL, worker_routine, (void *)msg2);
if (status1 != 0) {
printf("Failed to spawn worker thread A\n");
} else {
printf("Worker thread A spawned\n");
}
if (status2 != 0) {
printf("Failed to spawn worker thread B\n");
} else {
printf("Worker thread B spawned\n");
}
status1 = pthread_join(tid1, &exit_status);
printf("Worker A join status: %d\n", status1);
if (status1 != 0) {
printf("Error joining worker A\n");
}
printf("Worker A terminated\n");
status2 = pthread_join(tid2, &exit_status);
printf("Worker B join status: %d\n", status2);
if (status2 != 0) {
printf("Error joining worker B\n");
}
printf("Worker B terminated\n");
return 0;
}
void *worker_routine(void *arg)
{
int counter;
const char *label = (const char *)arg;
for (counter = 0; counter < 5; counter++) {
sleep(2);
printf("%s iteration: %d\n", label, counter);
}
return NULL;
}
Compilation
gcc thread_demo.c -o demo -pthread
Note: The -pthread flag is mandatory. Omitting it produces linker errors:
undefined reference to `pthread_create`
undefined reference to `pthread_join`
Alternatively, -lpthread works aswell.
Sample Output
Worker thread A spawned
Worker thread B spawned
Worker-B iteration: 0
Worker-A iteration: 0
Worker-A iteration: 1
Worker-B iteration: 1
Worker-B iteration: 2
Worker-A iteration: 2
Worker-A iteration: 3
Worker-B iteration: 3
Worker-B iteration: 4
Worker-A iteration: 4
Worker A join status: 0
Worker A terminated
Worker B join status: 0
Worker B terminated
The interleaved output demonstrates that thread execution order is non-deterministic, controlled entirely by the system's thread scheduler.