5. Multithreaded-Programming
5.1 Basic Concepts of Multithreading Programming
A thread is one of the fundamental units for implementing multitasking in a computer system. It represents an execution flow within a process. A process can contain multiple threads, and each thread can run independently while sharing the same memory space and system resources of the process.
Threads are lightweight, with lower overhead for creation and destruction compared to processes. Additionally, thread context switches are faster than process switches, making them a preferred choice when simultaneous execution of multiple tasks is required. Using threads can improve the performance and responsiveness of a program.
Threads are typically scheduled and executed by the operating system, and they can be coordinated and synchronized using various mechanisms such as mutexes, condition variables, and semaphores. There are two main implementations of threads: user-level threads, which are managed by the application itself, and kernel-level threads, which are managed by the operating system.
5.2 Benefits of Using Multithreading
Increased Concurrency and Responsiveness: Multithreading enables multiple tasks in a program to run in parallel, thereby enhancing the program's concurrency and responsiveness. For instance, in a network server, multiple threads can be employed to handle client requests, improving the server's processing capacity and response time.
Reduced System Overhead: Threads run within the same process, resulting in lower overhead for context switching and communication compared to multiple processes. Inter-process communication (IPC) mechanisms are required for data transfer and synchronization between multiple processes, while threads can directly access shared memory, reducing the need for IPC overhead.
Utilization of Multi-core CPUs: Multithreading can effectively utilize the performance of multi-core CPUs, allowing multiple threads to execute in parallel on different CPU cores, thereby improving program performance. For example, in intensive computing scenarios like image processing and video encoding, multithreading can accelerate computations.
Improved Resource Utilization: Multithreading allows threads to share the same code segment, data segment, and heap space of a process, reducing resource wastage. For instance, in a file compression program, multithreading can process multiple files concurrently, enhancing disk and CPU utilization.
Simplified Program Design: Multithreading can break down complex tasks into multiple simpler subtasks, each handled by a separate thread, simplifying program design and implementation. For instance, in a graphical user interface application, multithreading can separate the interface handling from background tasks, making the program more organized and maintainable.
However, it is essential to be cautious when using multithreading due to potential issues like thread safety, resource contention, and deadlocks. Careful design and synchronization mechanisms are necessary to ensure proper collaboration among threads. While multithreading brings benefits to application development, it is not suitable for all situations. Appropriate use cases for multithreading include, but are not limited to:
- Concurrent execution of multiple tasks
- Handling time-consuming tasks
- Tasks with varying priority levels
- Implementing asynchronous operations
5.3 Thread States
A thread goes through a lifecycle from creation to termination, always being in one of the following states:
- Ready State: When a thread is ready to run but hasn't been scheduled for execution yet, it is in the ready state. At this point, the thread has been allocated all the necessary system resources and is waiting for the operating system's scheduler to assign CPU resources.
- Running State: When a thread has been scheduled for execution and is actively running, it is in the running state.
- Blocked State: When a thread needs to wait for certain events to occur, such as waiting for I/O operations to complete, waiting for a semaphore, or waiting for a lock, the thread enters the blocked state. In this state, the thread does not consume CPU resources.
- Terminated State: When a thread has finished executing or encounters an unhandled exception, it enters the terminated state. At this point, the system resources occupied by the thread are released, and the thread object is destroyed.
5.4 Thread Identification
In multi-threaded programming, a thread's identifier (ID) is used to uniquely identify a thread. The thread ID is typically an integer value generated by the operating system kernel and returned by library functions provided by the programming language. The thread ID exists from the thread's creation and disappears automatically once the thread ends.
5.5 Creating Threads
In Linux systems, multi-threading follows the POSIX standard. Before delving into development, let's familiarize ourselves with some POSIX thread API functions:
| API Function | Description |
|---|---|
| pthread_create | Create a new thread |
| pthread_join | Wait for a thread to finish and retrieve its return value |
| pthread_self | Get the thread ID |
| pthread_cancel | Cancel another thread |
| pthread_exit | Call to exit the thread function within a thread |
| pthread_kill | Send a signal to a thread |
5.5.1 pthread_create Function
To create a thread using the POSIX API, the function pthread_create() is used. Its declaration is as follows:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Parameters:
- thread: A pointer to the thread identifier
- attr: Set the thread attributes; details will be explained in the next subsection
- start_routine: A function pointer that points to the thread's entry point, i.e., the function code to be executed when the thread runs
- arg: The parameter passed to the running thread
- Return Value: If the thread is created successfully, it returns 0. If the thread creation fails, it returns the corresponding error code
- Return Value: If the thread is created successfully, it returns 0. If the thread creation fails, it returns the corresponding error code.
5.5.2 pthread_join Function
The pthread_join() function is used in the POSIX API to wait for a specified thread to finish and retrieve its exit status. Its declaration is as follows:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
Parameters:
- thread: The identifier of the thread being waited for.
- retval: A pointer to a pointer, used to store the exit status of the thread being waited for.
- If you do not need to retrieve the exit status of the thread, you can set retval to NULL.
5.5.3 pthread_exit Function
The pthread_exit() function is used in the POSIX API to explicitly terminate the current thread's execution and exit the thread. Its declaration is as follows:
#include <pthread.h>
void pthread_exit(void *retval);
- Parameters:
- retval: A pointer to a data of any type representing the thread's exit status.
- If you do not need to retrieve the thread's exit status, you can set retval to NULL.
If a return statement is encountered during thread execution, it will also terminate the execution. Since the return keyword also applies to thread functions, why does the <pthread.h> header provide pthread_exit() function? The differences between them are as follows:
The main difference between return (void)0; and pthread_exit((void)0) is that the return statement only exits the current thread from the thread function, while the pthread_exit() function can be used at any time to explicitly exit the thread.
The pthread_exit() function also provides the feature of thread cleanup processing, allowing you to call all registered thread cleanup functions upon thread exit, which better manages thread resources.
5.5.4 Thread Application Example
Creating a thread and passing an integer parameter:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *thread_func(void *arg)
{
int *count = (int*)arg;
for (int i = 1; i <= *count; ++i)
{
printf("Thread: %d\n", i);
sleep(1);
}
pthread_exit(NULL);
}
int main(int argc, char *argv[])
{
pthread_t tid;
int ret;
int count = 5;
ret = pthread_create(&tid, NULL, thread_func, &count);
if (ret != 0)
{
printf("pthread_create failed:%d\n", ret);
return -1;
}
printf("create treads success\n");
ret = pthread_join(tid, NULL);
if ( ret != 0)
{
printf("pthread_join");
return -1;
}
printf("Thread finished.\n");
return 0;
}
Compile and run the program:
linaro@linaro-alip:~/Linux/thread$ gcc -o thread thread.c -lpthread
linaro@linaro-alip:~/Linux/thread$ ./thread
create treads success
Thread: 1
Thread: 2
Thread: 3
Thread: 4
Thread: 5
Thread finished.
linaro@linaro-alip:~/Linux/thread$
5.6 Thread Attributes
The POSIX standard defines multiple attributes for threads, including: detach state, scheduling policy and parameters, scope, stack size, stack address, priority, and more.
5.6.1 Detach State
In the POSIX thread library, a thread can be either detached or non-detached. The detach state of a thread determines how the thread's resources are handled when it terminates. If a thread is detached, it won't leave behind any resources, including the thread ID and memory resources. This means it cannot be joined by other threads to obtain its termination status, nor can its status be queried. The detach state of a thread can be set using the pthread_attr_setdetachstate() function, with the options PTHREAD_CREATE_JOINABLE for non-detached and PTHREAD_CREATE_DETACHED for detached state. Generally, if a thread only performs independent tasks and doesn't need to be joined by other threads, setting it to detached state can reduce resource consumption. However, it is essential to handle the thread termination appropriately in the code for detached threads since there is no automatic resource cleanup.
Setting the detach state of a thread using thread attributes:
Initialize thread attributes:
int pthread_attr_init(pthread_attr_t *attr);Destroy the resources associated with thread attributes:
int pthread_attr_destroy(pthread_attr_t *attr);- Return value: Success: 0; Failure: error code
Set the detach state of the thread (detached or non-detached):
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);- attr: Pointer to initialized thread attributes (input parameter)
- detachstate:
PTHREAD_CREATE_DETACHEDfor detached thread orPTHREAD_CREATE_JOINABLEfor non-detached thread
Create a child thread with its detach state set to
PTHREAD_CREATE_DETACHED, ensuring automatic resource release upon thread termination:#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *thread_func(void *arg)
{
printf("Child thread is running.\n");
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
pthread_attr_t attr;
int detachstate;
// Initialize thread attribute object
pthread_attr_init(&attr);
// Get the default detach state
pthread_attr_getdetachstate(&attr, &detachstate);
printf("Default detach state: %s\n", detachstate == PTHREAD_CREATE_JOINABLE ? "joinable" : "detached");
// Set the thread's detach state to detached
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid, &attr, thread_func, NULL);
// Destroy thread attribute object
pthread_attr_destroy(&attr);
// Allow some time for the child thread to run
sleep(1);
printf("Main thread is exiting.\n");
pthread_exit(NULL);
}
5.6.2 Stack Size
The stack is a data structure used to store information about function calls, local variables, and temporary data during the execution of a program. Each thread has its own stack to store information required during its execution.
The stack size refers to the amount of stack space allocated to a thread when it is created. The stack size directly affects the amount of data a thread can handle and its execution time. If the stack size is too small, it may cause a stack overflow and crash the program. On the other hand, if the stack size is too large, it will waste system resources.
When creating a thread, you can set the thread's stack size by specifying thread attributes. For example, in the POSIX thread library, you can use the pthread_attr_setstacksize() function to set the thread's stack size in bytes.
The stack size should be adjusted based on the specific needs of the application. If the application needs to handle large amounts of data or requires recursive function calls, a larger stack space may be necessary. Conversely, if the application only needs to handle small amounts of data, a smaller stack space can be used to save system resources.
In the POSIX thread library, you can use the following functions to set and get the thread's stack size in bytes:
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
Parameter description:
- attr: Pointer to the thread attributes
- stacksize: The size of the thread's stack in bytes
5.6.3 Scheduling Policy
The scheduling policy of a thread refers to how the operating system arranges the execution order of threads on the CPU. Different scheduling policies affect the thread's priority and execution behavior.
The POSIX thread library defines three scheduling policies:
- SCHED_FIFO (First In, First Out): Threads are executed in the order they are placed in the queue, and they run until they complete or are preempted. Threads with higher priority are executed first, and threads with the same priority are executed in the order they were placed in the queue
- SCHED_RR (Round Robin): Each thread is assigned a fixed time slice for execution. When the time slice is used up, the thread is moved to the back of the queue and waits for the next scheduling. Similar to SCHED_FIFO, threads with higher priority are executed first, and threads with the same priority are executed in the order they were placed in the queue
- SCHED_OTHER (Other): The system determines the thread's scheduling. This policy is often used as the default for multithreading programming and usually does not require explicit specification. The scheduling policy of a thread can be set using the
pthread_attr_setschedpolicy()function or dynamically modified using thepthread_setschedparam()function
In the POSIX thread library, you can use the following functions to get and set the scheduling policy and parameters of a thread:
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inheritsched);
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);
Parameter description:
- attr: Pointer to the thread attributes
- inheritsched: Whether the thread inherits the scheduling attributes. The options are:
PTHREAD_INHERIT_SCHED: The scheduling attributes are inherited from the creating thread, and the attributes set inattrwill be ignoredPTHREAD_EXPLICIT_SCHED: The scheduling attributes specified inattrwill be used
- policy: The scheduling policy of the thread, which can be
SCHED_OTHER,SCHED_FIFO, orSCHED_RR
Return value: If the function call is successful, it returns 0; otherwise, it returns the corresponding error code.
5.6.4 Priority
In Linux systems, thread priorities are divided into two types: static priority and dynamic priority.
Static priority is assigned when the thread is created and can be set using the
pthread_attr_setschedparam()function. Smaller static priority values indicate higher thread priority, and vice versa. For threads with the scheduling policySCHED_OTHER, their static priority must be set to 0, which is the default scheduling policy for Linux, and these threads are scheduled based on dynamic priority. In real-time scheduling policies, the static priority determines the basic scheduling order of real-time threads.Dynamic priority is adjusted dynamically at runtime based on the thread's behavior and resource requirements. In Linux systems, dynamic priority is calculated based on the nice value, which is an integer ranging from -20 to +19. A lower nice value indicates a higher priority, and a higher nice value indicates a lower priority. When a thread is ready to run but cannot be scheduled, its dynamic priority increases by one unit to ensure fair competition between such threads. When a thread is scheduled to run, its dynamic priority decreases to prevent one thread from occupying the CPU for an extended period and starving other threads.
To set the minimum and maximum priorities for threads, you can use the following functions:
int sched_get_priority_max(int policy);
int sched_get_priority_min(int policy);To obtain the minimum and maximum priorities available for threads under the three scheduling policies:
#include <stdio.h>
#include <unistd.h>
#include <sched.h>
int main()
{
printf("Valid priority range for SCHED_OTHER: %d - %d\n",
sched_get_priority_min(SCHED_OTHER),
sched_get_priority_max(SCHED_OTHER));
printf("Valid priority range for SCHED_FIFO: %d - %d\n",
sched_get_priority_min(SCHED_FIFO),
sched_get_priority_max(SCHED_FIFO));
printf("Valid priority range for SCHED_RR: %d - %d\n",
sched_get_priority_min(SCHED_RR),
sched_get_priority_max(SCHED_RR));
return 0;
}For the
SCHED_OTHERpolicy, priorities are not supported, whileSCHED_FIFOandSCHED_RRsupport priority usage. The priorities for these two policies range from 1 to 99, with larger values indicating higher priority. Setting and obtaining priorities can be done using the following functions:int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);Set and obtain the scheduling parameters of the thread, including the scheduling policy and priority:
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
int main() {
pthread_attr_t attr;
struct sched_param param;
int policy;
// Initialize thread attribute object
pthread_attr_init(&attr);
// Get the default scheduling policy
pthread_attr_getschedpolicy(&attr, &policy);
// Print the default scheduling policy
if (policy == SCHED_OTHER) {
printf("Default scheduling policy is SCHED_OTHER\n");
} else if (policy == SCHED_RR) {
printf("Default scheduling policy is SCHED_RR\n");
} else if (policy == SCHED_FIFO) {
printf("Default scheduling policy is SCHED_FIFO\n");
}
// Get the default scheduling parameters
pthread_attr_getschedparam(&attr, ¶m);
// Print the default priority
printf("Default priority is %d\n", param.sched_priority);
// Set the thread's scheduling policy and priority
param.sched_priority = 50;
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
pthread_attr_setschedparam(&attr, ¶m);
// Get the new scheduling policy and priority
pthread_attr_getschedpolicy(&attr, &policy);
pthread_attr_getschedparam(&attr, ¶m);
// Print the new scheduling policy and priority
if (policy == SCHED_OTHER) {
printf("New scheduling policy is SCHED_OTHER\n");
} else if (policy == SCHED_RR) {
printf("New scheduling policy is SCHED_RR\n");
} else if (policy == SCHED_FIFO) {
printf("New scheduling policy is SCHED_FIFO\n");
}
printf("New priority is %d\n", param.sched_priority);
// Destroy thread attribute object
pthread_attr_destroy(&attr);
return 0;
}
5.7 Thread Termination
Thread termination refers to the process in which a thread completes its task and exits, releasing the resources it occupied for other threads to use. Threads can terminate in two ways:
- Normal Termination: The thread exits by either calling the
pthread_exit()function or returning from the thread function. In the case of normal termination, the thread automatically releases the resources it occupied, including stack space, thread descriptor, and other resources. - Abnormal Termination: If a thread encounters an error during its execution, preventing it from continuing, it will terminate automatically. In this situation, it is essential to ensure that the thread releases all the resources it occupied to avoid issues like resource leaks and memory leaks.
In conclusion, thread termination is crucial as it prevents resource leaks and memory-related problems while improving system performance and stability. Therefore, when writing multi-threaded programs, it is essential to pay attention to thread termination, ensuring that threads exit gracefully and release all occupied resources.In the previous section on thread creation, we mentioned that a thread terminates its execution when encountering pthread_exit() or returning from the thread function.
5.7.1 pthread_cancel Function
In a multi-threaded program, one thread can send a "termination" signal (hereafter referred to as the "Cancel" signal) to another thread. This requires calling the pthread_cancel() function, which is declared as follows:
#include <pthread.h>
int pthread_cancel(pthread_t thread);
Before using the pthread_cancel() function, it is essential to understand the target thread's handling mechanism for the Cancel signal.
- For threads with default attributes, when a Cancel request (Cancel signal) is sent to the target thread, it does not immediately terminate. Instead, it responds to the Cancel signal and terminates execution when encountering a cancellation point.
- In the POSIX standard, functions that allow cancellation are referred to as "cancellation points" or "cancellable functions". Cancellation points are specific code locations during program execution where a thread can be canceled, and cleanup work can be performed. Many standard system calls and library functions are cancellation points, such as I/O functions and memory allocation functions. Common examples include
pthread_join(),pthread_testcancel(),sleep(), andsystem().
5.7.2 pthread_setcancelstate Function
If you want to manually modify the target thread's handling of the Cancel signal, you can use the pthread_setcancelstate() and pthread_setcanceltype() functions.
pthread_setcancelstate() function declaration:
#include <pthread.h>
int pthread_setcancelstate( int state , int * oldstate );
Parameter Explanation:
- The
stateparameter has two possible values:PTHREAD_CANCEL_ENABLE(default): The current thread will process Cancel signals sent by other threadsPTHREAD_CANCEL_DISABLE: The current thread ignores Cancel signals from other threads until the thread state is reset toPTHREAD_CANCEL_ENABLE, at which point it processes any received Cancel signals
- The
oldstateparameter is used to receive the thread's previousstatevalue, usually for resetting the thread. If you don't need to receive this parameter's value, set it to NULL.
- The
Return Value: When the
pthread_setcancelstate()function executes successfully, it returns 0; otherwise, it returns a non-zero value.
5.7.3 pthread_setcanceltype Function
pthread_setcanceltype() function declaration:
#include <pthread.h>
int pthread_setcanceltype( int type , int * oldtype );
Parameter Explanation:
- The
typeparameter has two possible values:PTHREAD_CANCEL_DEFERRED(default): The thread terminates its execution when reaching a cancellation pointPTHREAD_CANCEL_ASYNCHRONOUS: The thread terminates immediately upon receiving a Cancel signal
- The
oldtypeparameter is used to receive the thread's previoustypevalue. If you don't need to receive this parameter's value, set it to NULL
- The
Return Value: When the
pthread_setcanceltype()function executes successfully, it returns 0; otherwise, it returns a non-zero valueCreating a thread
tidwith its cancellation state set toPTHREAD_CANCEL_DISABLEand cancellation type set toPTHREAD_CANCEL_ASYNCHRONOUS, the main thread sleeps for 2 seconds and then sends a cancellation request to the thread. The thread is then canceled.#include <pthread.h>
#include <stdio.h>
void* thread_func(void* arg)
{
int oldstate, oldtype;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldtype);
printf("Thread started.\n");
while (1)
{
printf("Thread running...\n");
}
printf("Thread ended.\n");
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
sleep(2);
pthread_cancel(tid);
printf("Thread cancelled.\n");
pthread_join(tid, NULL);
printf("Main thread ended.\n");
return 0;
}