Skip to main content

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

  1. 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.
  2. Running State: When a thread has been scheduled for execution and is actively running, it is in the running state.
  3. 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.
  4. 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 FunctionDescription
pthread_createCreate a new thread
pthread_joinWait for a thread to finish and retrieve its return value
pthread_selfGet the thread ID
pthread_cancelCancel another thread
pthread_exitCall to exit the thread function within a thread
pthread_killSend 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_DETACHED for detached thread or PTHREAD_CREATE_JOINABLE for 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 the pthread_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 in attr will be ignored
      • PTHREAD_EXPLICIT_SCHED: The scheduling attributes specified in attr will be used
    • policy: The scheduling policy of the thread, which can be SCHED_OTHER, SCHED_FIFO, or SCHED_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 policy SCHED_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_OTHER policy, priorities are not supported, while SCHED_FIFO and SCHED_RR support 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, &param);

    // 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, &param);

    // Get the new scheduling policy and priority
    pthread_attr_getschedpolicy(&attr, &policy);
    pthread_attr_getschedparam(&attr, &param);

    // 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(), and system().

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 state parameter has two possible values:
      • PTHREAD_CANCEL_ENABLE (default): The current thread will process Cancel signals sent by other threads
      • PTHREAD_CANCEL_DISABLE: The current thread ignores Cancel signals from other threads until the thread state is reset to PTHREAD_CANCEL_ENABLE, at which point it processes any received Cancel signals
    • The oldstate parameter is used to receive the thread's previous state value, usually for resetting the thread. If you don't need to receive this parameter's value, set it to NULL.
  • 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 type parameter has two possible values:
      • PTHREAD_CANCEL_DEFERRED (default): The thread terminates its execution when reaching a cancellation point
      • PTHREAD_CANCEL_ASYNCHRONOUS: The thread terminates immediately upon receiving a Cancel signal
    • The oldtype parameter is used to receive the thread's previous type value. If you don't need to receive this parameter's value, set it to NULL
  • Return Value: When the pthread_setcanceltype() function executes successfully, it returns 0; otherwise, it returns a non-zero value

  • Creating a thread tid with its cancellation state set to PTHREAD_CANCEL_DISABLE and cancellation type set to PTHREAD_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;
    }