Operating System (4) - Multithreaded Programming

An overview of the thread

1. What is a thread

A thread is an entity of a process and is the basic unit of CPU scheduling and dispatch . It is a basic unit smaller than a process that can run independently . A thread basically does not own system resources by itself, only a few resources that are necessary in operation (such as a program counter, a set of registers and stacks), but it shares all the resources owned by the process with other threads belonging to the same process .

2. Processes and threads

When a Linux process creates a new thread, the thread will have its own stack (because the thread has its own local variables) , but it shares global variables , file descriptors , signal handles, and current directory state with its creator . Therefore, the death of a thread is equivalent to the death of the entire process, so multi-process programs are more robust than multi-threaded programs.

There is a difference between creating a child process by fork and creating a thread in Linux: fork creates a copy of the process, this new process has its own variables and its own PID, its time scheduling is independent, and its execution is almost Completely independent of the parent process.

A process can be regarded as the basic unit of a resource, and a thread is the basic unit of program scheduling, and the threads within a process contribute to the time obtained by the process.

3. Why process

Compared with processes, it is a very "thrifty" way of multitasking. Under the linux system, starting a new process must assign it an independent address space, and create a lot of data to maintain its code segment, stack end and data segment.

Running multiple threads in a process that use the same address space between them, and the time required to switch between threads is less than the time required to switch between processes . The overhead of a process is about 30 times that of a thread.

A convenient communication mechanism between threads . For different processes, they have independent data spaces . Data transmission can only be done through inter-process communication. However , threads in the same process share data space , so a thread The data can be directly used by other threads.

Makes the CPU system more efficient. The operating system will ensure that when the number of threads is not greater than the number of CPUs, different threads run on different CPUs.

To improve the program structure, a long and complex process can be considered to be divided into multiple processes and become several independent or semi-independent running parts. Such a program is conducive to understanding and modification.

4. How to use

Multithreading under Linux system follows the POSIX thread interface, called pthread. To write multi-threaded programs under Linux, you need to use the header file pthread.h. libpthread.a is required when linking. Such as: gcc main.c -lpthread -o main

 

Second, multi-threaded programming

1. Thread creation

#include <pthread.h>

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

thread: pointer to pthread_t type, used to refer to the newly created thread

attr: used to set the attributes of the thread, generally NULL

void *(*start_routine)(void *) : thread function

arg : thread function arguments

2. Thread ID acquisition

include <pthread.h>

pthread_t pthread_self(void); //Return the ID of the current thread

int pthread_equal(pthread_t t1, pthread_t t2); // Compare whether the two thread IDs are the same, if they are equal, return 0

3. Thread termination

The thread itself calls pthread_exit

#include<pthread.h>

void pthread_exit(void * retval); //The thread terminates execution by calling the pthread_eixt function and returns a pointer to an object. Note: Do not return a pointer to a local variable

4. Thread waiting

#include <pthread.h>

int pthread_join(pthread_t tid, void **thread_return); //Block the calling thread until the specified thread terminates. tid: ID of the thread waiting to exit return: Thread exit returns value pointer 

5. Thread separation

The detached state of a thread determines how a thread terminates itself. In the above example, we use the default attribute of the thread, which is the non-detached state (that can be combined, joinable, and needs to be recycled). In this case, the original thread waits for the created thread to end; only when pthread_join( ) function returns, the created thread is terminated and the system resources occupied by it can be released. The separation thread is not like this. It is not waited by other threads. When its own operation ends, the thread is terminated and the system resources are released immediately. Programmers should choose the appropriate separation state according to their needs.

#include <pthread.h>

int pthread_detach(pthread_t thread); //Success returns 0, failure returns an error value.

For a thread that is not detached when it is created, pthread_join() or pthread_detach() must be called once, otherwise the unreleased resources will be left after the thread ends.    

A pthread_join() operation waiting for a thread to finish can be initiated by any thread in the same group, not necessarily the main thread. In addition, if the main thread exits, that is, the process exits, all threads also exit. 

 

3. Thread synchronization

When multiple threads share the same memory, you need to ensure that each thread sees consistent data. If each thread uses variables that no other thread will read or modify, then there is no consistency problem. Likewise, if a variable is read-only, there will be no consistency issues when multiple threads read the variable. However, when a thread can modify a variable, and other threads can read or modify the variable, these threads need to be synchronized to ensure that they do not access invalid values ​​when accessing the storage contents of the variable.

1. Semaphore

There are usually two types of semaphores: binary semaphores and counting semaphores. The binary semaphore has only two values ​​of 0 and 1, and the count semaphore has a larger value range.

Semaphores are generally used to protect a piece of code so that it can only be run by one thread of execution at a time. To accomplish this, binary semaphores can be used.

Sometimes it is desirable to allow a limited number of threads to execute a given piece of code, and a counting semaphore can be used.

(1) Initialization

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

sem points to the semaphore that needs to be initialized

value specifies the initial value of the semaphore

pshared indicates whether it is shared among multiple threads of a process or among multiple processes. If pshared is 0, the semaphore is shared by multiple threads of a process, and the semaphore (sem_t) should be placed in a location visible to all threads (global variable or dynamic allocation)

Returns 0 if the execution is successful, -1 if there is an error, and sets errno
(2) semaphore control

#include <semaphore.h>

int sem_post (sem_t * sem); // v

int sem_wait (sem_t * sem); // p

The function of sem_post is to add 1 to the value of the semaphore in an atomic operation. The sem_wait function decrements the value of the semaphore by 1 in an atomic operation, but it will wait until the semaphore has a non-zero value before starting the subtraction operation. For example, calling sem_wait on a semaphore with a value of 2, the thread will continue to execute, but the value of the semaphore will be decremented to 1. If sem_wait is called on a semaphore with a value of 0, this function will wait until another thread increases the value of the semaphore so that it is no longer 0.

  If two threads are simultaneously waiting on the sem_wait function for the same semaphore to become non-zero, then when the semaphore is incremented by 1 by the third thread, only one of the waiting threads will start decrementing the semaphore by 1 and continue execution, Another thread will continue to wait

(3) Destruction

#include <semaphore.h>

int sem_destroy (sem_t * sem);

The function of this function is to clean up the semaphore after it is used up, and to clean up the resources owned by the semaphore. If the semaphore you are trying to clean up is being awaited by some thread, you will get an error

2. Mutex

A mutex is conceptually similar to a binary semaphore, that is, a semaphore with an initial value of 1. After a mutex is acquired, it cannot be acquired again, so the acquisition and release of a mutex are often referred to as locking and unlocking operations.

 A mutex can only be released by the thread that acquired it, and if this principle is violated, the result is undefined.

A mutex is essentially a lock. Before accessing a shared resource, the mutex is locked, and the lock on the mutex is released after the access is completed. After locking the mutex, any other thread that tries to lock the mutex again will be blocked until the current thread releases the mutex.
 

The thread must wait for the mutex before taking out the data . If another process has obtained the mutex at this time, the thread will block and wait for the other to release the mutex before the thread can get the mutex. .

In Linux, a mutex is represented by the type pthread_mutex_t , which must be initialized before use:

(1) Initialization

For statically allocated mutexes, set to the default mutex object PTHREAD_MUTEX_INITIALIZER

 For dynamically allocated mutexes, after allocating memory (malloc), it is initialized through pthread_mutex_init, and pthread_mutex_destroy needs to be called before releasing memory (free).

#include <pthread.h>  int pthread_mutex_init(pthread_mutex_t  *mutex,const pthread_mutexattr_t *attr) ;

 //The first parameter points to the mutex to be initialized, and the second parameter points to a structure describing the properties of the mutex, which can be set to NULL, indicating the default property

int pthread_mutex_destroy(pthread_mutex_t *mutex);

(2) Operation

To access shared resources, use the mutex to lock, if the mutex is already locked, the calling thread will block until the mutex is unlocked.

int pthread_mutex_lock(pthread_mutex_t *mutex)

int pthread_mutex_trylock(pthread_mutex_t *mutex)

Return value: 0 is returned on success, and the error number is returned on error.    

trylock is a non-blocking calling mode. If the mutex is not locked, the trylock function will lock the mutex and gain access to shared resources; if the mutex is locked, the trylock function will not block waiting And directly return EBUSY, indicating that the shared resource is in a busy state.

 pthread_mutex_unlock(pthread_mutex_t *mutex)

Used to unlock the mutex pointed to by the mutex parameter. If the mutex is unlocked or not owned by the current thread at this time, the result is undefined. Therefore, mutexes must be paired on the same thread. After the operation is complete, the mutex must be unlocked. This way other threads waiting for the lock have a chance to acquire the lock, otherwise other threads will block forever.

(3) The difference between mutex and semaphore

Mutex is a key, only one person can enter, go in to lock, come out to unlock; sem is a room that can accommodate N people, if people are not satisfied, they can go in, if they are full, they have to wait for people to come out

The mutex is released by the thread that acquires the lock (who gets it, who releases it) , while the semaphore can be released by other threads  

The initial state may be different: the initial value of mutex is 1, and the initial value of sem may be 0 (or 1)

3. Condition variables

Suppose there is such a situation: a thread is waiting for a condition in the shared data to occur, and then it must be unlocked first, otherwise it is impossible for other threads to change the shared data. An implementation method is that the shared data can be detected cyclically, but it needs to be locked before the detection and unlocked after the detection, so the efficiency will be very low.
          Therefore, in this case, a method is needed such that the thread goes to sleep while waiting for some condition to be fulfilled, and once the condition is fulfilled, the thread should be woken up to continue execution. The way to do this is to use POSIX condition variables.  

Condition variables are not locks themselves, but they can also cause threads to block, and are usually used in conjunction with mutex locks to provide a place for multiple threads to turn around.

(1) Initialization

int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr);    

The cond parameter points to the condition variable to be initialized, and the attr parameter points to a structure describing the attributes of the condition variable. Attr can be NULL, indicating that the default attributes are used.

(2) wait

When the program needs to wait for a condition variable, the following function can be used:

int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,  const struct timespec *abstime);

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

These functions have a parameter mutex that points to the mutex, indicating that the condition variable must be used in conjunction with the mutex. When these functions are called, first, the specified mutex will be released, then the thread will block, waiting for the condition variable to fire. Before the function returns, the mutex is reacquired by the thread.

The difference between pthread_cond_timedwait() and pthread_cond_wait() is that the former has a timeout limit specified by abs time, and when the thread blocks for longer than this time, it will automatically wake up. 

(3) Trigger

To trigger a condition variable you can use the following functions:

int pthread_cond_broadcast(pthread_cond_t *cond);

int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_signal can wake up one or more threads waiting for the condition variable pointed to by the cond parameter, while pthread_cond_broadcast can wake up all threads waiting for the condition variable pointed to by the cond parameter.
Note: The POSIX standard only stipulates that the pthread_cond_signal function wakes up at least one sleeping thread, not only one.  

 

(4) Destruction

After the condition variable is not used, it should be destroyed with the following function:

int pthread_cond_destroy(pthread_cond_t *cond);  

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324468925&siteId=291194637