One article to understand Linux inter-thread communication/thread synchronization methods - mutex locks, read-write locks, spin locks, semaphores, condition variables, signals, etc.

Table of contents

Inter-thread communication/thread synchronization method

lock mechanism

Mutex

Read-write lock (rwlock)

Spin lock (spin)

Semaphore mechanism

condition variable mechanism

Signal


Inter-thread communication/thread synchronization method

ps. Many of the following paragraphs are direct quotations, without using markdown's "citation" format, and the sources have been released.

Reference/Citation:

Since process variable resources are shared between threads, the purpose of communication between threads is mainly for thread synchronization (that is, the order rules (semaphores) or mutual exclusion rules (mutex locks) that constrain the execution of multiple threads), so threads are not like processes A communication mechanism used for data exchange in communications.

The difference between mutex locks, condition variables and semaphores:

  • Mutual exclusion lock: Mutual exclusion. If one thread occupies a certain resource, other threads cannot access it. Only when this thread unlocks can other threads access it.

  • Semaphore: Synchronization. When one thread completes an action, it tells other threads through the semaphore, and the other threads then perform certain actions. And the semaphore has a more powerful function. The semaphore can be used as a resource counter. The value of the semaphore is initialized to the currently available number of a certain resource. It decreases after using one and increments after returning one.

  • Condition variable: Synchronization. When one thread completes an action, it sends a signal to other threads through the condition variable, and the other threads then perform certain actions. Condition variables must be used with mutex locks.

The locking mechanism is suitable for situations similar to atomic operations. After locking, the resources in a certain critical section are quickly processed and then unlocked immediately. It is not suitable for long-term locking (the worse case is that the resources in the critical section after locking are blocked). After executing the interrupt, the interrupt needs to be blocked and locked, then a deadlock occurs and the signal is stuck); and the signal/semaphore (and condition variable) is suitable for long-term waiting for the signal/condition to occur. And the caller sleeps during the waiting/blocking period.

In order to reduce errors and complexity, before designing the program, you should try to consider not using locks, signals, etc. in interrupts. The interrupt program should be designed concisely and elegantly.

lock mechanism

Mutex

Mutex lock/mutex is used to prevent multiple threads from accessing a critical resource at the same time/concurrently.

A mutex is essentially a lock. The mutex is locked before accessing a shared resource and released after the access is completed. After the mutex is locked, other threads that lock the mutex again will be blocked until the current thread releases the mutex lock. For mutex locks, if the resource is already occupied, the resource applicant can only enter the sleep state.

If more than one thread is blocked when the mutex is released, then all blocked threads on the lock will become runnable. The first thread to become runnable can lock the mutex, and other threads will see When the mutex is still locked, it can only block again and wait for it to become available again. In this way, only one thread can execute forward at a time.

That is, when multiple threads want to access a critical resource, such as a global variable, they need to access it mutually: when I access it, you cannot access it.

Header file: #include <pthread.h>.

Commonly used APIs:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr); // Initialization amount 
/* This function initializes a mutex. The first parameter is the mutex pointer, and the second parameter is the attribute that controls the mutex. Generally NULL. When the function is successful, it will return 0, indicating that the mutex is successfully initialized. 
    Of course, the initialized mutex can also be quickly initialized by calling a macro. The code is as follows: 
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER; 
*/ 
​int
pthread_mutex_lock(pthread_mutex_t *mutex); // Lock (block) 
int pthread_mutex_unlock(pthread_mutex_t *mutex); // Unlock (Non-blocking) 
/* The lock function and unlock function are lock and unlock functions respectively. You only need to pass in the initialized pthread_mutex_t mutex pointer. It will return 0 on success. 
    When a thread obtains the execution right, it executes the lock function. Once the lock is successfully obtained, other threads will be blocked when encountering the lock function until the thread acquiring the resource executes the unlock function. The unlock function wakes up other threads waiting for the mutex.
    Special note is that after acquiring the lock, unlock must be executed after the logical processing is completed, otherwise a deadlock will occur! As a result, the remaining threads have been blocked and unable to execute. When using a mutex, pay special attention to using the pthread_cancel function to prevent deadlock! 
* 
/
int pthread_mutex_trylock(pthread_mutex_t *mutex); // Mutex lock (non-blocking) 
/* This function is also a thread locking function, but this function is in non-blocking mode and uses the return value to determine whether the lock is successful. The usage is the same as above The blocking and locking functions are consistent. */ 
​int
pthread_mutex_destroy(pthread_mutex_t *mutex); // Destroy the mutex 
/* This function is used to destroy the mutex. By passing in the pointer of the mutex, the destruction of the mutex can be completed, and 0 is returned successfully. . */

Routine: Reference pthread库-线程编程例程-来自百问网\01_文档配套源码\Pthread_Text10.c.

Mutex lock properties:

/* Similar to thread attributes, first declare the variable, then initialize it with pthread_mutexattr_init, 
    then use pthread_mutexattr_getxxx/pthread_mutexattr_setxxx to get/set a certain option of the attribute, 
    then fill in the attribute when calling the mutex lock to initialize pthread_mutex_init, 
    and finally destroy it* / 
int pthread_mutexattr_init(pthread_mutexattr_t* attr); 
int pthread_mutexattr_destroy(pthread_mutexattr_t* attr); 
​int
pthread_mutexattr_getshared(const pthread_mutexattr_t* attr, int* pshared); int pthread_mutexattr_setshared(pthread_mutexattr _t* attr, int* pshared) 
; 
    pshared parameters that can be passed in : 
        PTHREAD_PROCESS_SHARED: The mutex lock can be shared across processes. 
        PTHREAD_PROCESS_PRIVATE: It can only be shared by threads in the process to which the initializing thread belongs.  
int pthread_mutexattr_gettype(const pthread_mutexattr_t* attr, int* type);
int pthread_mutexattr_settype(pthread_mutexattr_t* attr, int type); 
    Type parameters that can be passed in:
        PTHREAD_MUTEX_NOMAL: Fair lock. Locking a normal lock that has been locked again will cause a deadlock; unlocking a normal lock that has been locked by another thread, or unlocking a normal lock that has been unlocked again will lead to unpredictable results. s consequence. 
        PTHREAD_MUTEX_ERRORCHECK: Error-checking lock. If an already-locked error-checking lock is locked again, the locking operation returns EDEADLOCK. To unlock an error-checking lock that has been locked by another thread, or to unlock an already-unlocked error-checking lock again, the unlocking operation returns EPERM. 
        PTHREAD_MUTEX_RECURSIVE: Nested lock, error use returns EPERM 
        PTHREAD_MUTEX_DEFAULT: Similar to nominal.
Read-write lock (rwlock)

 Read-write locks are similar to mutexes, but read-write locks have higher parallelism. A mutex is either locked or unlocked, and only one thread can lock it at a time. The read-write lock has three states: locked state in read mode, locked state in write mode, and unlocked state .

Only one thread can hold a read-write lock in write mode at a time, but multiple threads can hold a read-write lock in read mode at the same time. When the read-write lock is in the write-locked state, all threads trying to lock the lock will be blocked until the lock is unlocked. When the read-write lock is in the read-locked state, all threads that attempt to lock it in read mode can obtain access, but any thread that wishes to lock the lock in write mode will block until all threads release it. until their read lock.

Header file: #include <pthread.h>.

Commonly used APIs:

int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *rwlockattr); // Initialize read and write lock 
int
pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // Read mode lock read and write lock 
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // Write mode Lock read Write lock 
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); // Unlock the read and write lock 
int
pthread_rwlock_destroy(pthread_rwlock_t *rwlock); // Destroy the read and write lock
Spin lock (spin)

If the mutex is occupied (locked) and another thread enters, the mutex will cause thread switching (not waiting, but yielding once to run other threads). There are many contents suitable for locking.

For spin locks, if the lock is occupied (locked), the incoming thread will wait until acquiring the lock is equivalent to while(1);(dead wait, running for a time slice). When the content suitable for the lock is relatively small and the cost of thread switching is far greater than the cost of waiting, use a spin lock. (If you know something about RTOS, you will understand the concepts of yeild, running a full time slice, etc. You can understand it by playing FreeRTOS and RTT on a microcontroller).

A spin lock is similar to a mutex, but instead of blocking the process by sleeping, it remains in a busy waiting (spin) blocking state before acquiring the lock. Spin locks can be used in the following situations: the lock is held for a short time and the thread does not want to spend too much cost on rescheduling.

If an executable thread attempts to acquire a contended (already held) spin lock, the thread will always be busy waiting, spinning, that is, idling, waiting for the lock to be available again (waiting to be unlocked). A contended spin lock causes the thread requesting it to spin while waiting for the lock to become available again, which wastes CPU time, so spin locks should not be held for long periods of time. In fact, this is the original intention of the spin lock design, to perform lightweight locking in a short time. Spin locks cannot be used in interrupt programs. Preemption is disabled while the spin lock is held (the kernel is not allowed to be preempted).

Header file: #include <pthread.h>.

Commonly used APIs:

int pthread_spin_init(pthread_spinlock_t *lock, int pshared); 
/* Spin lock initialization function, there is no attribute variable in it, pshared can select parameters: 
    PTHREAD_PROCESS_PRIVATE Use this lock only in this process 
    PTHREAD_PROCESS_SHARED This lock may be located in a shared memory object, in Sharing between multiple processes 
    If you want to use spin locks to synchronize multiple processes, set pshared to PTHREAD_PROCESS_SHARED, and then allocate the pthread_spinlock_t object in the process shared memory (the same is true for pthread_mutex_t). 
*/ 
​int
pthread_spin_destroy(pthread_spinlock_t *lock); 
​//
Spin lock operation 
int pthread_spin_lock(pthread_spinlock_t *lock); 
int pthread_spin_trylock(pthread_spinlock_t *lock); 
int pthread_spin_unlock(pthread_spinlock_t *lock);

Semaphore mechanism

The semaphore is a quantity greater than or equal to 0. When it is 0, the thread blocks. Use the sem_pos function (non-blocking) to increase the semaphore by 1. The sem_wait function (blocking) decreases the semaphore by 1 when it is greater than 0, and blocks when it is equal to 0.

It can be used for notifications or counting:

  • Semaphores can control the execution order of multiple threads that are originally executed randomly and out of order in a controllable and predictable manner. For example, thread A is waiting for something, and thread B can send a signal to thread A after it completes the thing.

  • A "semaphore" object can be used when a counter is needed to limit the number of threads that can use a shared resource. The semaphore stores the count value of the threads currently accessing a specified resource. The count value is the number of threads that can still use the resource. If this count reaches zero, all access attempts to the resource controlled by this semaphore are placed in a queue and wait until timeout or the count value is non-zero.

Header file: #include <semaphore.h>.

Commonly used APIs:

int sem_init(sem_t *sem, int pshared, unsigned int value); //Initialize a semaphore 
/* This function can initialize a semaphore, and the first parameter is passed in a sem_t type pointer. 
    The second parameter passed in 0 represents thread control (used within multiple threads), otherwise it is process control (used by multiple processes). 
    The third parameter represents the initial value of the semaphore, 0 represents blocking, 1 and higher numbers represent the initial value (non-blocking). 
    After the initialization of the semaphore is completed, 0 will be returned if the execution is successful. */ 
int sem_destroy(sem_t * sem); // Destroy the semaphore 
​int
sem_wait(sem_t * sem); // Decrease the semaphore by 1 (blocking), P operation 
int sem_post(sem_t * sem); // Increase the semaphore by 1 (non-blocking), V operation 
int sem_trywait(sem_t *sem); // Decrease the semaphore by 1 (non-blocking), return 0 successfully 
/* The sem_wait function is used to detect whether resources are available for the specified semaphore. If no resources are available, it will block. Wait, if resources are available, the sem - 1 operation will be automatically performed. 
   The sem_post function will release the resources of the specified semaphore and perform the sem + 1 operation. 
   That is, the application and release of semaphores*/
sem_timedwait (sem_t * sem,const struct timespec * abstime); // During the waiting/blocking period of sem_wait, wait up to abstime seconds before returning 
int sem_post_multiple (sem_t * sem,int count); // Once the semaphore is increased count (non-blocking) 
int
sem_getvalue(sem_t * sem, int * sval); // Get the value of the current semaphore

Routines: Reference pthread库-线程编程例程-来自百问网\01_文档配套源码\Pthread_Text12.c, pthread库-线程编程例程-来自百问网\02_视频配套源码\pthread3.c 和 pthread4.c.

condition variable mechanism

A condition variable is a synchronization mechanism used to notify other threads that a condition is met, that is, a thread is suspended until an event occurs. It is generally used to notify the other party of the status information of shared data, so condition variables are used in conjunction with mutexes.

Condition variables are a commonly used method to implement "wait--->wake up" logic in multi-threaded programs.

Header file: #include <pthread.h>.

Commonly used APIs:

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); // Initialize condition variable 
/* If no attribute needs to be set, fill cond_attr as N ULL */ 
/* Another way to initialize pthread_cond_t by default cond = PTHREAD_COND_INITIALIZER; */ 
int pthread_cond_destroy (pthread_cond_t *cond); // Destroy condition variable 
int
Pthread_wait (pthread_cond_t *cond, pthread_mutex_t *mutex); t PTHread_Cond_Timewait (pthread_cond_t *cond, pthread_mutex_t *mutex 
, const struct timeSpec *TSPTR ); // Within a given time, wait for the condition variable to become true 
int
pthread_cond_signal(pthread_cond_t *cond); // Notify the condition variable that the condition is met. The pthread_cond_signal function will only wake up a thread waiting for the cond condition variable. 
Use
: 
    
    send Signal: 
    pthread_mutex_lock(&mutex); lock first
        Modify the critical resource here, then modify the resource 
    pthread_cond_signal(&cond); Then send the condition variable signal 
    pthread_mutex_unlock(&mutex); Then unlock 
and wait for
    the signal: 
    pthread_mutex_lock(&mutex); Lock first/* Lock when you can lock and then run later, Otherwise block */ 
    pthread_cond_wait(&cond, &mutex); and then wait for the condition variable signal/* If the condition is not met, the mutex will be unlocked first, and then block/sleep here to wait for the condition to be met; when the condition is met, it will be awakened, first lock the mutex and then Unblock and execute later*/ 
        Modify the critical resource here, then modify the resource 
    pthread_mutex_unlock(&mutex); and then unlock 
    ...

Routine: Reference pthread库-线程编程例程-来自百问网\02_视频配套源码\pthread5.c.

Signal

Handling signals between multiple threads in a process is different from signal processing in the process. It requires more learning and use.

For example, when you send a SIGSTP signal to a process, all threads of the process will stop. Because all threads use the same memory space, the handlers for a signal are the same, but different threads have different management structures, so different threads can have different masks (quoted from Linux Thread Implementation & LinuxThread vs. NPTL & user-level kernel-level threads & threads and signal processing - blcblc - Blog Park (cnblogs.com) ).

reference:

Guess you like

Origin blog.csdn.net/Staokgo/article/details/132630769