Basic concepts of Linux multithreaded programming

Keywords: Linux

Thread is the smallest unit that the operating system can perform scheduling operation. It is included in the process and is the actual operation unit in the process. A thread refers to a single sequential control flow in a process. Multiple threads can be concurrent in a process, and each thread executes different tasks in parallel.

linux operating system uses POSIX thread as the system standard thread, which defines a complete set of API for operating threads.

Thread creation

The life cycle of a thread starts from the moment it is created. Create the thread interface:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

Function Description:

POSIX thread is abbreviated as pthread; pthread_t is a variable of type unsigned long int, which is used to represent the ID of the thread

Function parameters:

Thread (output parameter), by pthread_create the thread handle returned after the thread is successfully created, which is used to mark the new thread in the API of subsequent operation threads;
start_ Routine (input parameter) to create an entry function of a new thread;
Arg (input parameter), the parameter passed to the new thread entry function;
Attr (input parameter), which specifies the attributes of the new thread, such as the thread stack size; If the value is NULL, the system default property is used.
Function return value:

Success, return 0; Failed, return relevant error code.

Note:

  • Main thread, which is the initial thread of a process, and its entry function is the main function.
  • The running time of a new thread. A thread may not be executed immediately after it is created, or even not executed after the thread that created it is finished; It is also possible for a new thread to start from pthread in the current thread_ It was already running before create, even in pthread_ The new thread has been executed before returning from the current thread before create.

Thread ID

After the new thread is created, it has a unique identifier in the process in which it is located (the thread is attached to the process), which is defined by pthread_t means, called thread ID. A thread can call the following interface to get its ID:

include <pthread.h>
pthread_t pthread_self(void);

pthread_self directly returns the ID of the calling thread.
Judging the size of two thread IDS is meaningless, but sometimes it may be necessary to judge whether two given thread IDs are equal. Use the following interface:

include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);

pthread_equal if the thread ID S specified by t1 and t2 are the same, a non-zero value is returned; Otherwise, 0 is returned.

Observe the creation of threads from the perspective of system implementation

To create a new thread, from the perspective of system implementation, is to create a new schedulable entity; Threads in the same process share most of the process resources, and only a small part of the information is unique to the thread, such as stack and thread specific data. The following figure (the picture comes from the Linux/UNIX system programming manual) shows the allocation of memory resources when a process has four threads in memory:

It can be seen that the threads in the same process share almost all memory resources except the stack. Sharing means that multiple threads can modify a memory area at the same time, and the modification is visible to all threads of the same process.

Thread termination

The termination of a thread can be divided into two forms: passive termination and active termination

There are two ways of passive termination:

  1. If the process where the thread is located terminates and any thread executes the exit function, the process will terminate, resulting in the termination of all threads attached to the process.
  2. Pthread is called by other threads_ Cancel requests that the thread be canceled.

There are also two ways of active termination:

  1. Executing the return statement in the thread's entry function and the return statement in the main function (main thread entry function) will cause the process to terminate, resulting in the termination of all threads attached to the process.
  2. Thread calls pthread_exit function, main function (main thread entry function) calls pthread_exit function, the main thread terminates, but if there are other threads in the process, the process will continue to exist and other threads in the process will continue to run.

Thread termination function:

include <pthread.h>
void pthread_exit(void *retval);
  • Thread calls pthread_ The exit function will cause the calling thread to terminate and return the content specified by retval (how to obtain the return value will be described later).
  • Note: retval cannot point to the stack space of the thread, otherwise it may become a wild pointer!

Management thread termination

Thread connection
The termination of one thread is an asynchronous event for another thread. Sometimes we want to wait for the thread with a certain ID to terminate before performing some operations, pthread_ The join function provides us with this function, which is called thread connection:

include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

Parameter Description:

Thread (input parameter), which specifies the thread we want to wait for
Return (output parameter), the return value of the thread we are waiting for when it terminates, that is, the value of return in the thread entry function or calling pthread_ Parameters of the exit function
Return value:

  • 0 is returned on success
  • When an error occurs, a positive error code is returned

When thread X connects to thread Y, if thread Y is still running, thread X will block until thread Y terminates; If thread Y has terminated before being connected, the connection call of thread X will return immediately.

Connection thread actually has another meaning. After a thread terminates, if no one connects it, the system will not recover the resources occupied by the terminated thread, and the terminated thread will also become a zombie thread. Therefore, when we connect to a thread, we are actually telling the system that the resources of the terminated thread can be recycled.

Note: for a thread that has already been connected, performing the connection operation again will lead to unpredictable behavior!

Thread separation
Sometimes we don't care whether a thread has terminated or not. We just hope that if a thread terminates, the system can automatically recover the resources occupied by the terminated thread. pthread_ The detach function provides us with this function, which is called thread separation:

#include <pthread.h>
int pthread_detach(pthread_t thread);

Parameter Description:

Thread (input parameter), which specifies the thread that you want to perform the detach operation
Return value:

  • 0 is returned on success
  • When an error occurs, a positive error code is returned

By default, when a thread terminates, it needs to be connected before the system can recover its resources. If we call pthread_ The detach function separates a thread, and the system will automatically recycle its resources after the thread terminates.

Note: if a thread has been separated, we can't connect it anymore!

Basic operation code

/*
* File name: thread_sample1.c
* Description: demonstrates the basic operation of threads
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

/*Subthread 1 entry function*/
void *thread_routine1(void *arg)
{
    fprintf(stdout, "thread1: hello world!\n");
    sleep(1);
    /*Child thread 1 exits here*/
    return NULL;
}

/*Subthread 2 entry function*/
void *thread_routine2(void *arg)
{

    fprintf(stdout, "thread2: I'm running...\n");
    pthread_t main_thread = (pthread_t)arg;

    /*Separate yourself and can no longer be connected*/
    pthread_detach(pthread_self());

    /*Judge whether the main thread ID is equal to the child thread 2ID*/
    if (!pthread_equal(main_thread, pthread_self())) {
        fprintf(stdout, "thread2: main thread id is not equal thread2\n");
    }

    /*Wait for the main thread to terminate*/
    pthread_join(main_thread, NULL);
    fprintf(stdout, "thread2: main thread exit!\n");

    fprintf(stdout, "thread2: exit!\n");
    fprintf(stdout, "thread2: process exit!\n");
    /*Child thread 2 terminates here and the process exits*/
    pthread_exit(NULL);
}

int main(int argc, char *argv[])
{

    /*Create child thread 1*/
    pthread_t t1;
    if (pthread_create(&t1, NULL, thread_routine1, NULL)!=0) {
        fprintf(stderr, "create thread fail.\n");
        exit(-1);
    }
    /*Wait for child thread 1 to terminate*/
    pthread_join(t1, NULL);
    fprintf(stdout, "main thread: thread1 terminated!\n\n");

    /*Create child thread 2 and pass the main thread ID to child thread 2*/
    pthread_t t2;
    if (pthread_create(&t2, NULL, thread_routine2, (void *)pthread_self())!=0) {
        fprintf(stderr, "create thread fail.\n");
        exit(-1);
    }

    fprintf(stdout, "main thread: sleeping...\n");
    sleep(3);
    /*The main thread uses pthread_ The exit function terminates and the process continues to exist*/
    fprintf(stdout, "main thread: exit!\n");
    pthread_exit(NULL);

    fprintf(stdout, "main thread: never reach here!\n");
    return 0;
}

Compilation and operation:

Posted by Aro on Sat, 20 Nov 2021 10:29:03 -0800