Embedded Linux: Thread Creation, Termination, Reclamation, Cancellation, and Detachment

Embedded Linux: Thread Creation, Termination, Reclamation, Cancellation, and Detachment

Click the blue text above to follow us

The operations of thread creation, termination, cancellation, reclamation, and detachment are core to multithreaded programming.

Embedded Linux: Thread Creation, Termination, Reclamation, Cancellation, and Detachment

In multithreaded programming, it is essential to manage the lifecycle of threads properly to avoid issues such as resource leaks, race conditions, or zombie threads.

1

Creating Threads

In Linux, by default, a process starts as a single-threaded application, and this thread is called the main thread.

However, modern computational tasks often require parallel processing, and the main thread can create additional threads using the pthread_create() function to execute tasks in parallel.

These additional threads share the process’s resources (such as memory space, file descriptors, etc.) with the main thread, but they have independent execution paths.

The definition of the pthread_create() function is as follows:

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,                   void *(*start_routine) (void *), void *arg);

Function parameters:

  • thread: This is a pointer of type pthread_t, pointing to a variable that stores the thread ID. pthread_t is the type used to uniquely identify a thread. When a thread is created successfully, this variable will be assigned the new thread’s ID for use in subsequent thread management.

  • attr: This is a pointer to a pthread_attr_t type, used to set the thread’s attributes. If set to NULL, the thread will use default attributes. These attributes include whether the thread is detached, stack size, etc. If specific attributes need to be set, pthread_attr_init() and related attribute functions can be used.

  • start_routine: This is a pointer to the thread execution function. After the new thread is created, it will start executing from this function. The function must conform to the following prototype:

void* (*start_routine)(void *arg);

It takes a void* type parameter (arg) and returns a void* type return value.

  • arg: This is the parameter passed to the start_routine function. It can be a pointer of any type. If no parameter needs to be passed, it can be set to NULL. If multiple parameters need to be passed, they can be packaged in a structure and passed through this pointer.

Return value:

  • Returns 0 on success.

  • Returns an error number on failure, indicating the reason for failure. For example, EAGAIN indicates that there are insufficient system resources to create a new thread, and EINVAL indicates that the provided attributes are invalid.

Key points for creating threads:

  • Thread ID: Each thread has a unique ID used to distinguish threads. When creating a thread, pthread_create() will store the new thread’s ID in a pthread_t type variable for subsequent operations.

  • Thread attributes: By default, threads use the system’s default attributes. If you need to change a thread’s attributes, such as setting it to detached or specifying the thread’s stack size, you can set it using pthread_attr_t.

  • Start function and parameters: The new thread will start executing from the start_routine function and pass the arg parameter. You can package multiple parameters in a structure and pass them to this function.

Once a new thread is created, it immediately joins the system’s thread scheduling queue, and will gain CPU execution time at an appropriate moment.

Since scheduling is controlled by the operating system, it is unpredictable which will execute first, the newly created thread or the main thread.

If the program has strict requirements on the execution order of threads, synchronization mechanisms (such as mutexes or semaphores) can be used to control the execution order between threads.

Here is a simple example of creating a thread and passing parameters:

void* thread_function(void* arg) {    int* num = (int*)arg;    printf("New thread running with argument: %d\n", *num);    return NULL;}
int main() {    pthread_t thread;    int arg = 42;
    // Create thread and pass parameter arg    if (pthread_create(&thread, NULL, thread_function, &arg) != 0) {        perror("pthread_create failed");        return 1;    }
    // Wait for new thread to finish    pthread_join(thread, NULL);
    printf("Main thread finished\n");    return 0;}

Explanation:

  • The thread function thread_function() receives a void* type parameter and casts it to an int* type to print the passed value.

  • The main thread calls pthread_create() to create a new thread and passes arg as a parameter to the new thread’s function thread_function().

  • After creating the thread, the main thread calls pthread_join() to wait for the new thread to finish execution. If pthread_join() is not used, the main thread will not wait for the new thread to finish, which may cause the program to exit prematurely.

2

Terminating Threads

In Linux, threads can be terminated in various ways, and different methods affect the thread’s exit behavior and process state management.

We will detail several common methods for terminating threads.

  • return: When the start function of the thread executes return, the thread terminates normally and returns the specified value, which can be obtained through pthread_join().

  • pthread_exit(): A thread can explicitly call pthread_exit() to terminate itself, allowing the thread to exit at any point, and the return value can also be obtained through pthread_join().

  • pthread_cancel(): A thread can be requested to be canceled through pthread_cancel(), but the thread must respond to the cancellation request to terminate.

  • exit() and _exit(): When any thread in the process calls exit(), _exit(), or _Exit(), the entire process, including all threads, will be terminated.

2.1, Exiting a Thread via the return Statement

The thread’s start function (the function passed to pthread_create()) can directly use the return statement to exit when it finishes executing. This method results in normal thread termination and returns the value as the thread’s exit code.

This is similar to calling pthread_exit().

void* thread_function(void* arg) {    // Perform some tasks    int result = 42;    return (void*)result; // Exit thread via return statement}

In the code above, the thread exits thread_function() and returns result via return, and this return value can be obtained in the main thread using pthread_join().

2.2, Exiting a Thread via pthread_exit()

pthread_exit() is specifically designed for exiting threads, allowing threads to explicitly exit at any point rather than relying on return.

After calling pthread_exit(), the control flow of the thread will immediately end, and no subsequent code will be executed.

The prototype of pthread_exit() is as follows:

void pthread_exit(void *retval);

Parameter retval: retval is a pointer of type void*, specifying the return value of the thread, which can be obtained by other threads through pthread_join().

An example is as follows:

void* thread_function(void* arg) {    int* retval = (int*)arg;    printf("Thread exiting with value: %d\n", *retval);    pthread_exit(retval); // Explicitly exit thread and return value}
int main() {    pthread_t thread;    int arg = 42;    int* retval;
    pthread_create(&thread, NULL, thread_function, &arg);    pthread_join(thread, (void**)&retval); // Get thread's exit code
    printf("Thread returned: %d\n", *retval);    return 0;}

Explanation:

  • In this example, pthread_exit() explicitly terminates the thread and returns the value of the parameter arg.

  • The main thread obtains the exit code of the child thread through pthread_join().

2.3, Terminating the Entire Process via exit(), _exit(), or _Exit()

exit(), _exit(), and _Exit() are not used to terminate a single thread but to terminate the entire process.

Since threads share the same process resources, if any thread calls these functions, the entire process (including all threads) will be terminated.

  • exit(): Normally terminates the process, executing cleanup functions and closing file descriptors, etc.

  • _exit() and _Exit(): Immediately terminate the process without executing cleanup work.

In the following example, calling exit() in thread_function() causes the entire process to terminate, and the main thread will not continue executing.

void* thread_function(void* arg) {    printf("Thread running...\n");    exit(0); // Call exit(), causing the entire process to terminate    return NULL;}
int main() {    pthread_t thread;
    pthread_create(&thread, NULL, thread_function, NULL);    pthread_join(thread, NULL);
    // If not terminated by exit(), the main thread would continue executing this line
    printf("Main thread finished\n");
    return 0;}

3

Reclaiming Threads

In multithreaded programming, when a thread ends, the resources it occupies are not immediately released by the system unless explicitly reclaimed. Otherwise, these threads will become zombie threads.

In Linux, reclaiming threads is similar to reclaiming processes.

Just as a parent process can use wait() to reclaim the resources of child processes, threads need to use pthread_join() to reclaim thread resources and obtain the thread’s exit status.

pthread_join() is a function used to wait for a specified thread to terminate and reclaim its resources. It will block the calling thread until the target thread terminates.

If the thread has already terminated, pthread_join() will return immediately.

Through this function, the main thread or other threads can obtain the exit status of the target thread and clean up its resources, avoiding the generation of zombie threads.

The prototype of pthread_join() is as follows:

int pthread_join(pthread_t thread, void **retval);

Function parameter description:

  • thread: This is the thread ID of the target thread, and pthread_join() will wait for this thread to terminate.

  • retval: This is a pointer to a void* type, pointing to the memory address that saves the thread return value. If the target thread returns a value via pthread_exit() or return statement, this value will be stored in the memory pointed to by *retval. If retval is NULL, it indicates that the return value of the target thread is not of concern.

Return value:

  • Returns 0 on success.

  • If the call fails, pthread_join() will return an error code. For example, ESRCH indicates that the specified thread does not exist, and EINVAL indicates that the thread cannot be reclaimed by pthread_join(), or the calling thread attempts to wait for itself to terminate.

In the following example, the thread executes thread_function() and returns result via pthread_exit().

The main thread calls pthread_join() to wait for the thread to finish and successfully obtains the thread’s return value.

void* thread_function(void* arg) {    int result = 100;    printf("Thread running...\n");    pthread_exit((void*)&result); // Explicitly return a result}
int main() {    pthread_t thread;    int* thread_result;
    // Create thread    pthread_create(&thread, NULL, thread_function, NULL);
    // Reclaim thread and get return value    pthread_join(thread, (void**)&thread_result);
    printf("Thread returned: %d\n", *thread_result);    return 0;}

3.1, Usage Scenarios and Precautions for pthread_join()

pthread_join() is a blocking function, which will wait for the specified thread to finish. If the target thread needs to perform extensive computations or processing, the calling thread will remain in a waiting state until the target thread terminates.

If the thread has already finished, pthread_join() will return immediately.

In the following example, the main thread will wait for 5 seconds when calling pthread_join() until worker_function() finishes executing.

void* worker_function(void* arg) {    sleep(5);  // Simulate some long-running operation    return NULL;}
int main() {    pthread_t thread;
    pthread_create(&thread, NULL, worker_function, NULL);
    printf("Waiting for thread to finish...\n");    pthread_join(thread, NULL); // Block waiting for thread to finish
    printf("Thread finished.\n");    return 0;}

In processes, if the parent process does not reclaim the child processes, the child processes will become zombie processes, occupying system resources.

Similarly, if a thread terminates and is not reclaimed by other threads calling pthread_join(), its memory and other resources will not be immediately released, leading to the zombie thread issue.

Zombie threads not only waste resources, but if too many accumulate, they may prevent the application from creating new threads.

3.2, Differences Between pthread_join() and Process Reclamation

Although pthread_join() is similar to waitpid() in processes, both are used to wait for child threads (or child processes) to finish and obtain their exit status, there are some significant differences between the two:

1. Thread relationships are equal.

In multithreaded programs, any thread can call pthread_join() to wait for another thread’s termination. Even if it is not the thread that created that thread, it can call pthread_join() to wait for its termination. There is no parent-child hierarchy among threads.

For example, if thread A creates thread B, and thread B creates thread C, then thread A can wait for thread C to finish without relying on thread B.

This is different from the parent-child hierarchy in processes, where the parent process is the only one that can call wait() or waitpid() to wait for the termination of child processes.

2. pthread_join() does not support non-blocking waits.

pthread_join() is a blocking call and does not support a non-blocking mode like waitpid() (achieved by passing the WNOHANG flag).

This means that the thread calling pthread_join() must wait for the target thread to terminate and cannot perform other operations. If more complex thread synchronization is needed, other mechanisms such as semaphores, condition variables, etc., are usually required.

4

Cancelling Threads

Generally, threads will decide when to end themselves, either by calling pthread_exit() or executing return statements in their start function.

However, in some scenarios, the main thread or other threads may need to force terminate a running thread, which can be achieved through a cancellation request.

By calling pthread_cancel(), a cancellation request can be sent to the target thread, asking it to terminate.

The prototype of pthread_cancel() is as follows:

int pthread_cancel(pthread_t thread);

Parameter description:

  • thread: The thread ID of the target thread to be canceled.

Return value:

  • Returns 0 on success.

  • If the call fails, it returns an error code, for example: ESRCH: The specified thread does not exist.

4.1, Cancellation Response Mechanism of Threads

The target thread can decide how to respond to cancellation requests.Each thread has a cancellation state and a cancellation type that control its response to cancellation requests:

4.1.1, Cancellation State

The cancellation state determines whether a thread is allowed to respond to cancellation requests. A thread can modify its cancellation state by calling pthread_setcancelstate().

  • PTHREAD_CANCEL_ENABLE: Indicates that the thread allows responses to cancellation requests (this is the default state).

  • PTHREAD_CANCEL_DISABLE: Indicates that the thread does not allow responses to cancellation requests. Even if a cancellation request is received, the thread will continue to run until its cancellation state is reset to allow cancellation.

The prototype of pthread_setcancelstate() is as follows:

int pthread_setcancelstate(int state, int *oldstate);

Parameters:

  • state: Can be PTHREAD_CANCEL_ENABLE or PTHREAD_CANCEL_DISABLE, indicating whether to enable or disable responses to cancellation requests.

  • oldstate: If not NULL, will save the original cancellation state.

An example is as follows:

pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); // Disable cancellation requests
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);  // Enable cancellation requests

4.1.2, Cancellation Type

The cancellation type determines when a thread responds to cancellation requests.

The cancellation type can be set by calling pthread_setcanceltype():

  • PTHREAD_CANCEL_DEFERRED: The thread will respond to cancellation requests at certain specific cancellation points (for example, when calling pthread_testcancel(), or during I/O operations). This is the default cancellation type.

  • PTHREAD_CANCEL_ASYNCHRONOUS: The thread will respond immediately upon receiving a cancellation request, which may cause the thread to be canceled at any point.

The prototype of pthread_setcanceltype() is as follows:

int pthread_setcanceltype(int type, int *oldtype);

Parameters:

  • type: Can be PTHREAD_CANCEL_DEFERRED or PTHREAD_CANCEL_ASYNCHRONOUS, indicating whether to respond to cancellation requests with delay or immediately.

  • oldtype: If not NULL, will save the original cancellation type.

An example:

pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); // Set to respond to cancellation immediately
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);      // Set to respond to cancellation with delay

4.2, Cancellation Points and Thread Cleanup

When the thread’s cancellation type is set to PTHREAD_CANCEL_DEFERRED, the thread will only respond to cancellation requests when it reaches certain cancellation points.

Cancellation points are usually some time-consuming operations or system calls, such as:

  • pthread_testcancel(): Explicitly sets a cancellation point.

  • Other common system calls, such as I/O operations, sleep(), select(), etc., are implicit cancellation points.

Embedded Linux: Thread Creation, Termination, Reclamation, Cancellation, and Detachment

There are many functions in the system that can serve as cancellation points, which will not be listed here one by one.You can refer to the man pages for more information by using the command man 7 pthreads.

The prototype of pthread_testcancel() is as follows:

void pthread_testcancel(void);

This function can explicitly create a cancellation point at any position in the code.

After calling pthread_testcancel(), the thread will check if there is a cancellation request, and if so, the thread will exit at this point.

An example is as follows:

while (1) {    // Perform some tasks    pthread_testcancel(); // Explicitly set cancellation point to check for cancellation requests}

4.3, Thread Cleanup Handler

During thread termination (whether normal or canceled), cleanup handlers can be used to clean up resources.

Cleanup handlers ensure that threads can properly release resources when canceled, avoiding resource leaks.

Use pthread_cleanup_push() and pthread_cleanup_pop() to set cleanup handlers:

  • pthread_cleanup_push(void (*routine)(void *), void *arg): Pushes a cleanup handler routine onto the stack, which will be called when the thread exits.

  • pthread_cleanup_pop(int execute): Pops the cleanup handler from the stack, where execute indicates whether to execute the handler.

In the following example, when the thread receives a cancellation request, it will respond to the request and exit at the pthread_testcancel() function. During thread exit, cleanup_handler() will be called to clean up resources.

void cleanup_handler(void *arg) {    printf("Cleanup: %s\n", (char *)arg);}
void* thread_function(void* arg) {    pthread_cleanup_push(cleanup_handler, "Thread resources"); // Set cleanup handler    while (1) {        printf("Thread running...\n");        sleep(1);        pthread_testcancel(); // Check for cancellation requests    }    pthread_cleanup_pop(1); // 1 indicates to execute cleanup handler    return NULL;}
int main() {    pthread_t thread;    pthread_create(&thread, NULL, thread_function, NULL);
    sleep(3);  // Wait a few seconds    pthread_cancel(thread); // Send cancellation request    pthread_join(thread, NULL); // Wait for thread to finish    printf("Thread has been canceled.\n");
    return 0;}

Correctly handling thread cancellation operations is crucial for complex multithreaded applications, especially when performing long-running tasks. Flexibly managing thread cancellation states and cleanup behaviors can effectively enhance the stability and reliability of the system.

  • pthread_cancel() is used to send a cancellation request to the target thread, asking it to terminate, but whether the target thread terminates depends on its cancellation state and cancellation type.

  • Threads can control whether to respond to cancellation requests via pthread_setcancelstate() and when to respond via pthread_setcanceltype().

  • In the case of deferred cancellation, threads will only check for cancellation requests at specific cancellation points, which can be explicitly set using pthread_testcancel().

  • Cleanup handlers ensure that threads can correctly release resources when canceled, avoiding resource leaks.

5

Detaching Threads

By default, after a thread terminates, its resources are not immediately reclaimed by the system unless another thread explicitly waits for the thread to terminate using the pthread_join() function.

However, if the exit status and return value of certain threads are not important to the program, and you do not wish to manually call pthread_join(), you can set that thread to detached state.

A thread in detached state will have its resources automatically reclaimed by the system upon termination.

To set a thread to detached state, you can call the pthread_detach() function.

The prototype of pthread_detach() is as follows:

int pthread_detach(pthread_t thread);

Parameter description:

  • thread: The thread ID of the target thread to be detached.

Return value:

  • Returns 0 on success.

  • If the call fails, it returns an error code, for example:

    • ESRCH: The specified thread does not exist or has already been reclaimed.

    • EINVAL: The thread is already in detached state.

After calling pthread_detach(), the specified thread will enter detached state.

Threads in detached state will have all their resources automatically reclaimed by the system upon termination, without the need for other threads to explicitly call pthread_join().

Detached state is irreversible, once a thread is detached, it cannot be recovered to a state that can be reclaimed by pthread_join().

In the following example, a new thread is created and set to detached state using pthread_detach(). After that, there is no need to call pthread_join(), and the system will automatically reclaim its resources when the thread terminates.

pthread_t thread;pthread_create(&thread, NULL, thread_function, NULL);pthread_detach(thread); // Set the thread to detached state

Threads can not only be detached by other threads but can also detach themselves by calling pthread_detach(pthread_self()).

This means that the thread does not need other threads to reclaim resources upon termination, and the system will handle it automatically.

An example is as follows:

void* thread_function(void* arg) {    pthread_detach(pthread_self()); // Detach itself    // Other operations performed by the thread    pthread_exit(NULL);}

The thread detachment mechanism is particularly suitable for the following scenarios:

  • Not concerned about thread return values: If the task performed by the thread does not require a return value and does not wish other threads to explicitly wait for its completion.

  • Avoiding zombie threads: Zombie threads refer to threads that have terminated but whose resources have not been reclaimed. Long-term existence of zombie threads consumes system resources. Setting threads to detached state can prevent the generation of zombie threads.

  • Long-running background tasks: If a thread runs for a long time or is a background task, and the main thread does not need to wait for its completion, detaching the thread can simplify resource management.

Comparison of thread detachment and pthread_join():

Thread Detachment:

  • Use pthread_detach() to set the thread to detached state.

  • The system automatically reclaims resources when the thread terminates.

  • Cannot obtain the return value or wait for the termination of the thread via pthread_join().

pthread_join():

  • Proactively call pthread_join() to wait for the specified thread to terminate and reclaim resources.

  • Can obtain the return value or termination status of the thread.

  • If pthread_join() is not called, the thread will become a zombie thread after termination until its resources are reclaimed.

Thread detachment is very useful in simplifying resource management in multithreaded programs, especially for threads that do not need to be waited for or reclaimed, optimizing the performance and stability of the program through the detachment mechanism.

Finally, two points of caution:

  • Irreversibility: Once a thread is set to detached state, it cannot be restored to a state that can be reclaimed by pthread_join(). If you detach a thread, you will not be able to obtain its return value or wait for its termination later.

  • Thread synchronization issues: If a task performed by a thread needs to be synchronized with other threads, it should not be detached. Otherwise, the main thread or other threads may continue executing without waiting for the thread to finish, leading to incomplete tasks.

Embedded Linux: Thread Creation, Termination, Reclamation, Cancellation, and Detachment
Embedded Linux: Thread Creation, Termination, Reclamation, Cancellation, and Detachment
Click Read Original for more exciting content~

Leave a Comment