Linux – multithreading

Table of Contents

1. What is a thread?

2. The main difference between processes and threads

3. Reasons for using threads

4. Thread related functions

4.1 Creation of threads

4.2 Thread exit

4.3 Thread waiting

4.4 The ID of the thread itself

4.5 Sample code

5. Mutex lock

5.1 Related functions (brief description)

5.2 Deadlock

5.3 Conditional lock


1. What is a thread

Thread refers to the smallest unit of concurrent execution between different execution paths in the operating system. In a multi-threaded system, each thread shares the memory space and resources of the same process and can directly access data and resources within the process. Data transmission and collaboration are carried out between threads through shared memory space, without the need to use an inter-process communication mechanism. Through the concurrent execution of threads, the efficiency and responsiveness of the program can be improved.

2. The main difference between process and thread

  1. Resource occupation: Each process has independent memory space, file descriptors and other system resources, while threads share these resources. Therefore, creating a new process requires allocating independent resources, while creating a thread is relatively lightweight.

  2. Switching overhead: When a process switches, it is necessary to save and restore the context of the entire process, including memory images, register states, etc., which is relatively expensive; Thread switching only requires saving and restoring the thread itself. context, less overhead.

  3. Concurrency: Different processes are executed concurrently, and they can be executed simultaneously on different processor cores. Threads execute concurrently within the same process, share the resources of the same process, and execute in turn on the processor through the scheduler to achieve concurrency.

  4. Communication method: Inter-process communication requires the use of specific mechanisms, such as pipes, message queues, shared memory, etc., for data exchange and collaboration. And the memory space in the process is shared between threads, and shared variables can be directly read and written to realize data sharing and communication.

  5. Error isolation: Since processes have independent memory spaces, when an error or crash occurs in one process, it will not affect the execution of other processes. And threads share resources of the same process, an error in one thread may cause the entire process to crash.

  6. Security: Since processes have independent memory spaces, the data between processes are independent of each other, so they are safer; and threads share the same memory space, so you need to pay attention to critical sections Protection to avoid problems such as data competition and deadlock.

3. Reasons for using threads

Reason one:

It is more resource efficient compared to processes. Under the Linux system, starting a new process requires allocating an independent address space and data table, which is an expensive operation; while multiple threads run in the same process and share most of the data with each other, the space required to start a thread is much smaller than There is space to start a process, and the time for thread switching is much shorter than the time for process switching.

Reason two:

Convenient communication mechanism between threads. Because the data space is shared between threads in the same process, the data of one thread can be directly used by other threads, which is fast and convenient. Of course, data sharing will also bring other problems. For example, synchronization and protection need to be paid attention to when multiple threads modify the same variable at the same time. Static variables also need to be paid attention to in multi-threaded programs.

4.1 Thread Creation

Header file #include
Function prototype

int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,

void *(*start_rtn)(void*),void *arg);

Parameters

tidp:

Pointer to the thread identifier.

attr:

Used to set thread properties. Usually NULL.

(*start_rtn)(void*):

The starting address of the thread running function.

arg:

Parameters to run the function.

Function Create thread
Return value

Success: Returns 0

Failure: Error number, and the contents of *thread are undefined.

Note:

The newly created thread starts running from the address of the start_rtn function, which has only one untyped pointer parameter arg. If you need to pass more than one parameter to the start_rtn function, you need to put these parameters into a structure, and then pass the address of this structure as the arg parameter.

4.2 Thread Exit

Header file #include
Function prototype void pthread_exit(void *retval);
Parameters

retval:

Indicates the thread exit status, usually NULL is passed.

Function Terminate the thread that called it and return A pointer to an object.
Return value

None

Three ways for threads to exit:

The thread returns from the startup function:

After a thread has completed its tasks, it can exit by returning from the startup function. The thread’s exit code can be passed as a return value to the parent thread or other threads for them to obtain the thread’s exit status.

Thread canceled by other thread:

In the same process, a thread can terminate the execution of the thread by sending a cancellation request to the target thread. It should be noted that the thread needs to explicitly check the cancellation request and handle it accordingly to ensure that the thread can exit correctly.

The thread calls pthread_exit():

The thread can actively call the pthread_exit() function to exit the thread. This function accepts one parameter as the exit status of the thread. Calling pthread_exit() causes the thread to exit immediately, with the specified exit status passed to the parent thread or other threads waiting for the thread to end.

Note:

In either method, when the thread exits, it should be sure to release the resources occupied by it to avoid resource leaks. In addition, for threads that use dynamic memory allocation, you need to pay attention to releasing the corresponding memory space before exiting.

4.3 Thread Waiting

Header file #include
Function prototype int pthread_join(pthread_t thread, void **retval);
Parameters

thread:

The thread identifier, or thread ID, identifies a unique thread.

retval:

User-defined pointer used to store the return value of the waiting thread.

Function Wait for thread specification in a blocking manner The thread ends
Return value

Success: Returns 0

Failure: Returns an error number.

Note:

  1. pthread_join() can only be used on threads that are already in the joinable state. When creating a thread, you can specify the detached status (joinable or detached) of the thread by setting the thread attribute. By default, the thread is joinable.

  2. Make sure the target thread has exited. If the target thread has not exited, calling pthread_join() will block forever. Therefore, before calling pthread_join(), you need to ensure that the target thread has completed execution or been canceled.

  3. Possible performance impact. Since pthread_join() blocks the current thread, calling pthread_join() frequently in the main thread may affect the performance of the program. If you do not need to wait for the thread to exit, you can set the thread to a detached state, or use other methods for thread management.

  4. If you are not interested in the return value of the thread, can set rval_ptr to NULL. In this case, calling the pthread_join function will wait for the specified thread to terminate, but will not obtain the thread’s termination status.

4.4 Thread’s own ID

Header file #include
Function prototype pthread_t pthread_self(void);
Parameters

None

Function Get the ID of the thread itself
Return value

Success: Returns the identifier of the current thread

4.5 sample code

#include <stdio.h>
#include <pthread.h>


void *func(void *argv)
{
static char *str = "t1 pthread quit!!!";
printf("t1 receive: %s\
",(char *)argv);
printf("t1 pthread id is %lu\
",pthread_self());

//Thread exits
//void pthread_exit(void *retval);
pthread_exit((void *)str);

}

int main()
{
//Thread creation
//int pthread_create(pthread_t *thread,
                         const pthread_attr_t *attr,
                         void *(*start_routine) (void *),
                         void *arg);

char *str = "hello";
pthread_t t1;
\t
int ret = pthread_create( & amp;t1,NULL,func,(void *)str);
\t
    if(ret != 0)
printf("create the pthread is fail\
");
printf("main pthread id is %lu\
",pthread_self());


//Thread waits
/*The thread calling this function will be blocked until the specified thread calls pthread_exit.
      Returned from the startup routine or was canceled. */
//int pthread_join(pthread_t thread, void **retval);
    
char *msg = NULL;
pthread_join(t1,(void **) & amp;msg);
\t
printf("t1 pthread quit after returning the value: %s\
",msg);

return 0;
}

5. Mutex lock

Mutex is a synchronization mechanism used to protect critical sections in multi-thread programming. It is mainly used to prevent data inconsistency and race conditions caused by multiple threads accessing shared resources at the same time.

Mutex locks provide mutually exclusive access to shared resources, ensuring that only one thread can access the protected critical section at any time. When using a mutex, you need to first create and initialize the mutex, and then perform locking and unlocking operations to ensure thread safety.

pthread_mutex_init initializes a mutex lock
pthread_mutex_destroy unregisters a mutex lock
pthread_mutex_lock locks successfully, if not successful, blocks and waits.
pthread_mutex_unlock unlock operation
#include <stdio.h>
#include<pthread.h>
#include <unistd.h>

pthread_mutex_t mutex;

void *func1(void *argv)
{
//Lock. After adding the lock, wait for this thread to finish executing. After unlocking, other threads can lock.
//int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_lock( & amp;mutex);
printf("t1 pthread create success!\
");
printf("t1: pthread id is %ld\
",pthread_self());
//Unlock
//int pthread_mutex_unlock(pthread_mutex_t mutex);
pthread_mutex_unlock( & amp;mutex);

}

void *func2(void *argv)
{
//Lock
//int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_lock( & amp;mutex);
printf("t2 pthread create success!\
");
printf("t2: pthread id is %ld\
",pthread_self());
//Unlock
//int pthread_mutex_unlock(pthread_mutex_t mutex);
pthread_mutex_unlock( & amp;mutex);
}
int main()
{
//Understand the use of mutex locks
\t
//Create mutex lock
//int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
pthread_mutex_init( & amp;mutex,NULL);


pthread_t t1;
pthread_t t2;
int value = 100;
int ret;
ret = pthread_create( & amp;t1,NULL,func1,(void *) & amp;value);
ret = pthread_create( & amp;t2,NULL,func2,(void *) & amp;value);

printf("main: pthread id is %ld\
",pthread_self());

pthread_join(t1,NULL);
pthread_join(t2,NULL);
\t
//Destroy the lock
//int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_destroy( & amp;mutex);

return 0;
}

5.2 Deadlock

Deadlock refers to a situation where two or more processes (threads) are unable to continue execution because they are waiting for resources held by each other. Simply put, deadlock is a deadlock caused by resource competition and mutually exclusive access.

Deadlock usually occurs when the following four conditions are met at the same time:

  1. Mutually exclusive conditions: Some resources can only be occupied by one process (thread) at a time, that is, the resources cannot be shared.
  2. Request and hold conditions: While holding at least one resource, a process (thread) requests resources held by other processes (threads).
  3. Non-deprivation condition: Already allocated resources cannot be forcibly taken away from the owner and can only be explicitly released by the holder.
  4. Circular waiting conditions: There is a resource application sequence for a process (thread). Each process (thread) is waiting for the next process (thread) to release resources, forming a loop of circular waiting.

Sample code:

#include <stdio.h>
#include<pthread.h>
#include <unistd.h>

//Understand deadlock (requires more than two locks), that is, thread 1 wants to take my lock, thread 2 wants to take your lock, and the two threads are deadlocked.

//Create two locks
pthread_mutex_t mutex1;
pthread_mutex_t mutex2;

void *func1(void *argv)
{
//deadlock phenomenon
pthread_mutex_lock( & amp;mutex1);
sleep(1);
pthread_mutex_lock( & amp;mutex2);
\t
printf("t1 pthread create success!\
");
printf("t1: pthread id is %ld\
",pthread_self());
\t
pthread_mutex_unlock( & amp;mutex1);

}

void *func2(void *argv)
{
//deadlock phenomenon
pthread_mutex_lock( & amp;mutex2);
sleep(1);
pthread_mutex_lock( & amp;mutex1);
\t
printf("t2 pthread create success!\
");
printf("t2: pthread id is %ld\
",pthread_self());
\t
pthread_mutex_unlock( & amp;mutex1);
}
int main()
{
pthread_mutex_init( & amp;mutex1,NULL);
pthread_mutex_init( & amp;mutex2,NULL);

pthread_t t1;
pthread_t t2;
int value = 100;
int ret;
ret = pthread_create( & amp;t1,NULL,func1,(void *) & amp;value);
ret = pthread_create( & amp;t2,NULL,func2,(void *) & amp;value);

printf("main: pthread id is %ld\
",pthread_self());

pthread_join(t1,NULL);
pthread_join(t2,NULL);
\t
//Destroy the lock
//int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_destroy( & amp;mutex1);
pthread_mutex_destroy( & amp;mutex2);

return 0;
}

5.3 Conditional Lock

Conditional lock is a synchronization mechanism based on the extension of mutex lock. A conditional lock allows a thread to wait for a specific condition variable to be satisfied before continuing execution.

Condition variables are a synchronization mechanism for threads that provide a way for a thread to wait for a specific condition to occur. Condition variables are often used with mutex locks to wait for a specific condition to occur in a race-free manner. It is itself protected by a mutex. Before changing the condition state, the thread must first lock the mutex, so that other threads will not perceive the change in condition before acquiring the mutex, because the mutex needs to be locked before the condition can be checked.

Condition variables need to be initialized before use. Condition variables of type pthread_cond_t can be initialized in two ways. For statically allocated condition variables, you can assign the constant PTHREAD_COND_INITIALIZER to it; and For dynamically allocated condition variables, you can use the pthread_cond_destroy function for deinitialization.

Sample code:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
//Understand the use of conditions and mutex locks

int g_val = 0;

pthread_mutex_t mutex;
pthread_cond_t cond;

void *func1(void *argv)
{
static int cnt = 0;
while(1)
{
//Conditional waiting, blocking here, waiting for other threads to trigger conditions
//int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
pthread_cond_wait( & amp;cond, & amp;mutex);
sleep(1);
printf("t1:val %d\
---------------------\
",g_val);
g_val = 0;
if(cnt + + == 5)
exit(1);
}
}

void *func2(void *argv)
{
while(1)
{
pthread_mutex_lock( & amp;mutex);
printf("t2:val %d\
",g_val + + );
pthread_mutex_unlock( & amp;mutex);
if(g_val == 3)
{
//Conditional trigger
//int pthread_cond_signal(pthread_cond_t cond);
pthread_cond_signal( & amp;cond);
}
sleep(1);
}
}
int main()
{
\t
//Creation of conditions
//int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
pthread_cond_init( & amp;cond,NULL);
pthread_mutex_init( & amp;mutex,NULL);


pthread_t t1;
pthread_t t2;
int value = 100;
int ret;
ret = pthread_create( & amp;t1,NULL,func1,(void *) & amp;value);
ret = pthread_create( & amp;t2,NULL,func2,(void *) & amp;value);


pthread_join(t1,NULL);
pthread_join(t2,NULL);
\t
//Destruction conditions
//int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_destroy( & amp;cond);
pthread_mutex_destroy( & amp;mutex);

return 0;
}