Threads & Multithreading in Operating Systems
Learn how modern operating systems execute multiple tasks efficiently using threads. Understand the difference between processes and threads, why multithreading improves performance, and how Linux implements threads using POSIX Threads (Pthreads).
Learning Objectives
- Understand what a thread is.
- Learn why threads are needed.
- Differentiate between processes and threads.
- Understand thread life cycle.
- Learn the benefits of multithreading.
- Prepare for Linux Pthreads programming.
1. What is a Thread?
A thread is the smallest unit of CPU execution within a process. Every process contains at least one thread. When the operating system schedules work for the CPU, it schedules threads rather than the process as a whole.
A thread performs the actual execution of program instructions.
2. Why Are Threads Needed?
Many applications need to perform multiple activities at the same time. Examples include:
- A web browser downloading data while remaining responsive.
- A media player decoding audio while updating the user interface.
- A web server handling multiple client requests concurrently.
- An IDE compiling code while allowing the user to continue editing.
Instead of creating multiple processes, applications often create multiple threads within the same process.
3. Single-Threaded Process
Only one thread executes instructions. If this thread blocks while waiting for I/O, the process cannot execute additional application code until the thread becomes runnable again.
4. Multithreaded Process
Multiple threads belong to the same process. The operating system may schedule these threads independently. On multicore systems, multiple threads from the same process may execute simultaneously on different CPU cores.
5. Process vs Thread
| Process | Thread |
|---|---|
| Independent execution environment | Execution unit inside a process |
| Own virtual address space | Shares the process address space |
| Higher creation overhead | Lower creation overhead |
| Separate resources | Shares most process resources |
| Inter-process communication required | Direct communication through shared memory |
6. Resources Shared by Threads
Threads belonging to the same process share many resources.
- Virtual address space
- Global variables
- Heap memory
- Open file descriptors
- Signal dispositions
- Current working directory
7. Resources Not Shared by Threads
Each thread maintains its own execution context.
- Program Counter (Instruction Pointer)
- CPU Registers
- Stack
- Thread ID
- Scheduling information
- Thread-local storage
8. Why Does Every Thread Have Its Own Stack?
Each thread executes its own sequence of function calls. Local variables, return addresses, and function parameters are stored on the stack. If multiple threads shared a single stack, they would overwrite one another's execution data. Therefore, every thread has an independent stack.
9. Key Points
- A thread is the smallest schedulable execution unit.
- Every process contains at least one thread.
- Threads inside a process share most resources.
- Each thread has its own stack and CPU context.
- Multithreading improves responsiveness and resource utilization.
10. Interview Questions
- What is a thread?
- Why are threads lighter than processes?
- Which resources are shared among threads?
- Why does each thread require its own stack?
- Can two threads execute simultaneously on different CPU cores?
11. Thread Life Cycle
Like a process, a thread passes through different execution states during its lifetime.
The operating system scheduler moves threads between these states based on CPU availability, synchronization, and I/O events.
12. POSIX Threads (Pthreads)
Linux provides thread programming through the POSIX Threads (Pthreads) library. Pthreads is the standard API defined by POSIX for creating and managing threads in C programs.
13. Creating a Thread
A new thread is created using the pthread_create() function.
| Parameter | Description |
|---|---|
| thread | Receives the thread identifier. |
| attr | Thread attributes (NULL for default). |
| start_routine | Function executed by the new thread. |
| arg | Argument passed to the thread function. |
14. Execution Flow of pthread_create()
After successful creation, both the original thread and the new thread may execute concurrently. The scheduler determines when each thread runs.
15. First Pthread Program
16. Program Execution
17. Compiling Pthread Programs
Compile a pthread program using:
Run the executable:
18. Thread Termination
A thread may terminate in several ways:
- Return from the thread start routine.
- Call pthread_exit().
- Be cancelled by another thread.
- The process terminates.
A simple thread exit:
19. Waiting for a Thread
The pthread_join() function waits for another thread to finish execution.
The calling thread blocks until the specified thread terminates.
20. Key Points
- Linux uses the POSIX Threads (Pthreads) API.
pthread_create()creates a new thread.pthread_join()waits for a thread to finish.pthread_exit()terminates the calling thread.- The scheduler determines when each thread executes.
21. Interview Questions
- What is the purpose of
pthread_create()? - Why is
pthread_join()needed? - What happens if the main thread exits before worker threads finish?
- What are the parameters of
pthread_create()? - Why should Linux programs be compiled with
-pthread?
22. Thread Identifier (pthread_t)
Each POSIX thread has a unique thread identifier represented by the pthread_t data type. This identifier allows a program to refer to a specific thread when performing operations such as waiting for its completion or comparing it with another thread.
The actual representation of pthread_t is implementation
dependent and should be treated as an opaque type.
23. pthread_self()
The pthread_self() function returns the identifier of the calling thread.
24. pthread_equal()
Because pthread_t is an opaque type, thread identifiers
should be compared using pthread_equal() instead of the
== operator.
pthread_equal() makes the program portable across
POSIX implementations.
25. Joinable Threads
By default, a newly created POSIX thread is
joinable.
A joinable thread retains its termination information until another
thread calls pthread_join().
26. Detached Threads
A detached thread releases its resources automatically when it
terminates.
No other thread can successfully wait for a detached thread using
pthread_join().
27. pthread_detach()
A joinable thread can be converted into a detached thread by calling pthread_detach().
After a thread is detached, it cannot be joined later.
28. Returning a Value from a Thread
A thread may return a value either by returning from its start routine
or by calling pthread_exit().
The value can later be obtained using
pthread_join().
29. Receiving the Return Value
The second argument of pthread_join() receives the value
returned by the terminated thread.
30. Creating Multiple Threads
31. Key Points
pthread_tidentifies a POSIX thread.pthread_self()returns the calling thread's identifier.pthread_equal()is the portable way to compare thread IDs.- Threads are joinable by default.
- Detached threads automatically release their resources.
pthread_join()retrieves a thread's termination status.
32. Interview Questions
- What is the purpose of
pthread_t? - Why should
pthread_equal()be used instead of==? - What is the difference between joinable and detached threads?
- Can a detached thread be joined?
- How can a thread return a value to another thread?
33. Race Condition
A race condition occurs when multiple threads access the same shared data concurrently and at least one thread modifies that data without proper synchronization. The final program result may depend on the order in which the threads execute.
34. Race Condition Example
Suppose two threads increment the same counter.
This statement appears simple but is typically executed as multiple CPU operations:
If another thread executes these operations at the same time, one update may overwrite the other, resulting in an incorrect final value.
35. Critical Section
A critical section is a portion of code that accesses shared resources such as variables, files, or shared memory. Only one thread should execute a critical section at a time when concurrent access could lead to incorrect results.
36. Mutual Exclusion (Mutex)
Mutual exclusion ensures that only one thread at a time can execute a critical section. POSIX threads provide mutexes for implementing mutual exclusion.
37. pthread_mutex_t
A mutex object is represented by the pthread_mutex_t data type.
Before a mutex is used, it must be initialized.
38. Initializing a Mutex
A mutex may be initialized dynamically using pthread_mutex_init().
The second parameter specifies mutex attributes.
Passing NULL uses the default attributes.
39. Locking a Mutex
A thread enters a protected critical section by locking the mutex.
If another thread already holds the mutex, the calling thread blocks until the mutex becomes available.
40. Unlocking a Mutex
After completing the critical section, the thread releases the mutex.
41. First Mutex Program
42. Mutex Execution Flow
Because only one thread holds the mutex at a time, the shared counter is updated safely.
43. Key Points
- A race condition occurs when multiple threads access shared data without proper synchronization.
- A critical section accesses shared resources.
- Mutexes provide mutual exclusion.
pthread_mutex_lock()acquires a mutex.pthread_mutex_unlock()releases the mutex.- A mutex should be destroyed using
pthread_mutex_destroy()when it is no longer needed.
44. Interview Questions
- What is a race condition?
- What is a critical section?
- Why is a mutex required?
- What happens if two threads modify the same variable without synchronization?
- What is the purpose of
pthread_mutex_lock()? - Why should a mutex be unlocked?
- When should
pthread_mutex_destroy()be called?
45. Static Mutex Initialization
Instead of calling pthread_mutex_init(), a mutex with
default attributes can be initialized statically using
PTHREAD_MUTEX_INITIALIZER.
Static initialization is commonly used for global and static mutexes.
46. Mutex Attributes
Mutex behavior can be customized using mutex attributes.
Applications first initialize a
pthread_mutexattr_t object, configure its properties, and
then create a mutex using those attributes.
47. pthread_mutex_trylock()
Unlike pthread_mutex_lock(), which waits until the mutex
becomes available, pthread_mutex_trylock() attempts to
acquire the mutex immediately.
48. Blocking vs Non-blocking Locking
| Function | Behavior |
|---|---|
pthread_mutex_lock() |
Blocks until the mutex becomes available. |
pthread_mutex_trylock() |
Returns immediately if the mutex is unavailable. |
49. Deadlock
A deadlock occurs when two or more threads wait indefinitely for resources held by one another. As a result, none of the waiting threads can continue execution.
50. Deadlock Example
If both threads acquire their first mutex before requesting the second, each waits indefinitely for the other to release its mutex.
51. Preventing Deadlocks
Common techniques for reducing the likelihood of deadlocks include:
- Acquire multiple mutexes in a consistent order.
- Release locks as soon as possible.
- Avoid holding locks longer than necessary.
- Reduce the number of simultaneously held locks.
- Use
pthread_mutex_trylock()when appropriate to avoid waiting indefinitely.
52. Lock Ordering
One of the most effective deadlock prevention strategies is to acquire multiple mutexes in the same order throughout the program.
Consistent lock ordering prevents circular waiting among threads.
53. Multiple Mutex Example
Every thread in the program should follow the same lock acquisition order.
54. Key Points
PTHREAD_MUTEX_INITIALIZERprovides static initialization.pthread_mutex_trylock()never waits for a mutex.- Deadlocks occur when threads wait indefinitely for one another.
- Consistent lock ordering is a common deadlock prevention technique.
- Programs should minimize the time mutexes remain locked.
55. Interview Questions
- When should
PTHREAD_MUTEX_INITIALIZERbe used? - How does
pthread_mutex_trylock()differ frompthread_mutex_lock()? - What is a deadlock?
- Why does inconsistent lock ordering cause deadlocks?
- Name three techniques for preventing deadlocks.
- Why should critical sections be kept as short as possible?
56. Condition Variables
A condition variable allows threads to wait until a particular condition becomes true. Unlike a mutex, a condition variable does not protect shared data by itself. It is always used together with a mutex.
57. pthread_cond_t
A POSIX condition variable is represented by the pthread_cond_t type.
It may also be initialized dynamically using
pthread_cond_init().
58. pthread_cond_wait()
A thread waits for a condition using pthread_cond_wait().
This function performs three operations atomically:
- Releases the mutex.
- Puts the calling thread to sleep.
- Reacquires the mutex before returning.
59. pthread_cond_signal()
When one thread changes the shared state so that a waiting condition may now be true, it can wake one waiting thread using pthread_cond_signal().
60. pthread_cond_broadcast()
To wake all threads waiting on a condition variable, use pthread_cond_broadcast().
61. Producer–Consumer Synchronization
A classic application of condition variables is the Producer–Consumer problem.
The producer places data into the shared buffer. If the buffer is full, the producer waits. The consumer removes data from the buffer. If the buffer is empty, the consumer waits.
62. Producer–Consumer Execution Flow
63. Simplified Producer–Consumer Example
The waiting thread repeatedly checks the condition after waking before continuing.
64. Why Use a while Loop?
A thread should check the condition inside a while loop rather than an if statement.
- A thread may wake even though the condition is still false.
- Another thread may consume the shared resource before the awakened thread acquires the mutex.
- Rechecking the condition ensures correct synchronization.
65. Key Points
- Condition variables synchronize threads based on program state.
- A condition variable is always used together with a mutex.
pthread_cond_wait()releases the mutex while waiting and reacquires it before returning.pthread_cond_signal()wakes one waiting thread.pthread_cond_broadcast()wakes all waiting threads.- The waiting condition should be checked inside a
whileloop.
66. Interview Questions
- What problem do condition variables solve?
- Why must a condition variable be used with a mutex?
- What does
pthread_cond_wait()do atomically? - What is the difference between
pthread_cond_signal()andpthread_cond_broadcast()? - Why should a waiting thread use a
whileloop instead of anifstatement? - How are condition variables used in the Producer–Consumer problem?
67. Read–Write Locks
A Read–Write Lock allows multiple threads to read shared data simultaneously while ensuring that only one thread can modify the data at a time.
Read–Write locks improve performance when read operations occur much more frequently than write operations.
68. Reader–Writer Rules
| Operation | Allowed? |
|---|---|
| Multiple Readers | Yes |
| Reader + Writer | No |
| Multiple Writers | No |
69. pthread_rwlock_t
A POSIX Read–Write Lock is represented by the pthread_rwlock_t type.
The lock must be initialized before use.
70. pthread_rwlock_rdlock()
A thread acquires a read lock using pthread_rwlock_rdlock().
Multiple threads may hold read locks simultaneously, provided that no thread currently holds a write lock.
71. pthread_rwlock_wrlock()
A thread acquires exclusive access using pthread_rwlock_wrlock().
While a write lock is held, no other thread may obtain either a read or write lock.
72. pthread_rwlock_unlock()
Both read and write locks are released using the same function.
73. Read–Write Lock Example
74. Spin Locks
A spin lock is a synchronization mechanism in which a thread repeatedly checks whether a lock has become available instead of sleeping while waiting.
Spin locks are generally suitable only when the expected wait time is very short.
75. pthread_spinlock_t
Unlike a mutex, a waiting thread actively consumes CPU time while spinning.
76. Thread Barriers
A barrier synchronizes multiple threads by requiring them all to reach the same execution point before any of them continue.
77. pthread_barrier_t
The barrier count specifies the number of participating threads that must reach the barrier before any waiting thread proceeds.
78. Key Points
- Read–Write locks allow concurrent readers but only one writer.
- Readers cannot access shared data while a writer holds the write lock.
- Spin locks use busy waiting instead of sleeping.
- Spin locks are suitable only for short waiting periods.
- Barriers synchronize groups of threads at predefined execution points.
79. Interview Questions
- When should a Read–Write Lock be used instead of a mutex?
- Can multiple readers hold a read lock simultaneously?
- Can a writer execute while readers hold the lock?
- What is the difference between a mutex and a spin lock?
- Why do spin locks consume CPU time?
- What is the purpose of a thread barrier?
- What does the barrier count represent?
80. Thread-Specific Data (TSD)
Threads in the same process share global variables. However, there are situations where each thread needs its own private copy of a variable. POSIX provides Thread-Specific Data (TSD), also known as Thread-Local Storage (TLS), for this purpose.
81. pthread_key_t
A thread-specific data key is represented using the pthread_key_t type. Each thread can associate its own value with the same key.
82. pthread_key_create()
Create a thread-specific key using:
The optional second argument specifies a destructor function that is called automatically when a thread exits and has a non-NULL value associated with the key.
83. pthread_setspecific()
Each thread stores its own value independently, even though all threads use the same key.
84. pthread_getspecific()
The returned pointer corresponds to the value associated with the calling thread.
85. Thread Cleanup Handlers
Cleanup handlers allow a thread to automatically release resources when it exits or is cancelled. Typical cleanup tasks include:
- Unlocking mutexes
- Closing files
- Freeing dynamically allocated memory
- Releasing other system resources
86. pthread_cleanup_push()
The cleanup handler is executed when the thread exits or when
pthread_cleanup_pop() is called with a non-zero argument.
87. Thread Cancellation
One thread can request cancellation of another thread using pthread_cancel().
Cancellation is a request. The target thread responds according to its cancellation state and cancellation type.
88. Cancellation Points
A thread normally acts on a cancellation request when it reaches a cancellation point. Many blocking POSIX functions, such as waiting on a condition variable or reading from a file descriptor, are cancellation points.
89. Thread-Specific Data Example
Each thread prints the value stored in its own thread-specific storage.
90. Key Points
- Thread-specific data provides private storage for each thread.
pthread_key_create()creates a thread-specific key.pthread_setspecific()stores data for the calling thread.pthread_getspecific()retrieves the calling thread's stored data.- Cleanup handlers release resources during thread termination.
pthread_cancel()requests cancellation of another thread.- Cancellation normally occurs at defined cancellation points.
91. Interview Questions
- Why is Thread-Specific Data needed?
- What is the purpose of
pthread_key_t? - What is the role of the destructor passed to
pthread_key_create()? - What is the difference between global variables and thread-specific data?
- What is a cleanup handler?
- Why are cleanup handlers important in multithreaded programs?
- Does
pthread_cancel()immediately terminate a thread? - What is a cancellation point?
92. Thread Attributes (pthread_attr_t)
POSIX allows various properties of a thread to be configured before the thread is created. These properties are stored in a pthread_attr_t object.
If no custom attributes are required, NULL may be passed as
the attribute argument to pthread_create().
93. Creating a Thread with Attributes
After thread creation, the attribute object is no longer needed and should be destroyed.
94. Detached Thread Attribute
Instead of creating a joinable thread and later calling
pthread_detach(), a thread may be created in the detached
state.
95. Configuring Thread Stack Size
The default stack size is sufficient for many applications. Some applications that perform deep recursion or allocate large local objects may require a larger stack.
The example above requests a stack size of approximately 1 MB.
96. Thread Scheduling Attributes
POSIX thread attributes can also specify scheduling-related properties, such as scheduling policy and priority. Actual behavior depends on operating-system support and user privileges.
97. Best Practices for Multithreaded Programs
- Protect shared data with appropriate synchronization.
- Keep critical sections as short as possible.
- Acquire multiple locks in a consistent order.
- Release resources promptly.
- Prefer condition variables over busy waiting when threads need to wait.
- Join or detach every created thread.
- Check the return values of POSIX thread functions.
- Minimize shared mutable state where practical.
98. Common Programming Mistakes
| Mistake | Possible Result |
|---|---|
| Accessing shared data without synchronization | Race conditions |
| Forgetting to unlock a mutex | Deadlock or blocked threads |
| Returning the address of a local variable from a thread | Undefined behavior |
| Ignoring return values | Hidden runtime failures |
| Using excessive locking | Reduced concurrency and performance |
| Not joining or detaching threads | Thread resources remain allocated |
99. Complete Chapter Summary
- A thread is the smallest schedulable unit of execution.
- Threads within a process share most resources but maintain separate execution contexts.
- POSIX Threads (Pthreads) provide a portable API for multithreaded programming.
- Mutexes protect critical sections.
- Condition variables synchronize threads based on program state.
- Read–Write locks improve performance for read-dominant workloads.
- Spin locks use busy waiting and are appropriate only for short wait times.
- Barriers synchronize groups of threads.
- Thread-specific data provides private storage for each thread.
- Cleanup handlers assist with resource management during thread termination.
- Thread attributes configure behavior before thread creation.
100. Comprehensive Interview Questions
- What is the difference between a process and a thread?
- Which resources are shared among threads?
- Why does each thread require its own stack?
- Explain the purpose of
pthread_create(). - What is the difference between joinable and detached threads?
- What is a race condition?
- How does a mutex prevent race conditions?
- Why should
pthread_cond_wait()be used inside awhileloop? - When should a Read–Write Lock be preferred over a mutex?
- What is a deadlock, and how can it be avoided?
- What is Thread-Specific Data?
- What is the purpose of cleanup handlers?
- What is the difference between
pthread_mutex_lock()andpthread_mutex_trylock()? - Why are barriers useful?
- What are common mistakes in multithreaded programming?
Concept Mastery Assessment — Module 19
Verify your conceptual understanding with 12 targeted technical assessment questions. Instant feedback and explanations are provided for every choice.