Thread synchronization: mutexes, condition variables, spin locks, read-write locks

image

And I want to find Peng Zezai closer, and I am intoxicated with the chrysanthemum.


For a single-threaded process, it does not need to deal with thread synchronization , so thread synchronization is a problem that may need attention in a multi-threaded environment. The main advantage of threads lies in the sharing of resources, such as information sharing through global variables, but this convenient sharing comes at a price, that is, the problem of data inconsistency caused by multiple threads concurrently accessing shared data .

Discuss the following topics.

Why thread synchronization is needed;
thread synchronization mutex;
thread synchronization semaphore;
thread synchronization condition variable;
thread synchronization read-write lock.

1 Why thread synchronization is needed?

Thread synchronization is to protect access to shared resources . The shared resource mentioned here refers to the resource that multiple threads will access. For example, if a global variable a is defined, thread 1 accesses variable a, and thread 2 also accesses variable a, then variable a is multiple at this time. A shared resource between threads, everyone needs to access it.

The purpose of protection is to solve the problem of data consistency . Of course, under what circumstances will the problem of data consistency occur, and it will be distinguished according to different situations; ifVariables accessed by each thread are not read and modified by other threads(such as a local variable defined in a thread function or a global variable accessed by only one thread), then there is no problem of data consistency; similarly, ifvariable is read-only, there will be no data consistency problem when multiple threads read the variable at the same time; however, when a variable that can be modified by one thread can also be read or modified by other threads, there will be a problem of data consistency at this time , these threads need to be synchronized to ensure that they do not access invalid values ​​when accessing the storage content of variables.

The essence of the data consistency problem lies in the concurrent access (simultaneous access) of shared resources by multiple threads in the process . Multiple threads in a process are executed concurrently. Each thread is the basic unit of system calls and participates in the system scheduling queue; for shared resources among multiple threads, concurrent execution will lead to concurrent access to shared resources.The problem with concurrent access is competition(If multiple threads access shared resources at the same time, it means that there is competition, which is similar to competition in real life. For example, a captain needs to be selected in a team, and now there are two people in the candidate list, then means that there is a competitive relationship between these two people), concurrent access may cause data consistency problems, so this problem needs to be solved; to prevent concurrent access to shared resources, then it is necessary to protect access to shared resources to prevent concurrent access Access shared resources.

When one thread modifies a variable, other threads may see inconsistent values ​​when reading the variable. Figure 1.1 depicts a hypothetical example of two threads reading and writing the same variable (shared variable, shared resource). In this example, thread A reads the value of the variable, and then assigns a new value to the variable, but the write operation takes 2 clock cycles (this is just an assumption); when thread B reads in the middle of the two write cycles If this variable is deleted, it will get inconsistent values, which leads to the problem of data inconsistency.

insert image description here

We can write a simple code to test this file. The sample code 1.1 shows that two threads access shared resources in a normal way. The shared resources here refer to static global variables g_count. The program creates two threads, both of which execute the same function. This function executes a loop and repeats the following steps: copy the global variable g_countto the local variable l_countvariable, then increment l_count, and then l_countcopy back g_countto continuously increase the value g_countof the global variable value. Because l_countis an automatic variable (local variable defined in a function) allocated in the thread stack, each thread has a copy. The number of loop repetitions is either specified by command line parameters, or the default value is 10 million times. After the loop ends, the thread terminates. After the main thread recycles two threads, the value of the global variable is printed out g_count.

//示例代码 1.1
 #include <stdio.h>
 #include <stdlib.h>
 #include <pthread.h>
 #include <unistd.h>
 #include <string.h>
  
 static int loops;
 static int g_count = 0;
 
 static void *new_thread_start(void *arg)
 {
    
    
     int loops = *((int *)arg);
     int l_count, j;
     for (j = 0; j < loops; j++) {
    
    
         l_count = g_count;
         l_count++;
         g_count = l_count;
     }
     return (void *)0;
 }

 int main(int argc, char *argv[])
 {
    
    
     pthread_t tid1, tid2;
     int ret;
     /* 获取用户传递的参数 */
     if (2 > argc)
         loops = 10000000; //没有传递参数默认为 1000 万次
     else
         loops = atoi(argv[1]);

     /* 创建 2 个新线程 */
     ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
     if (ret) {
    
    
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
         exit(-1);
     }

     ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
     if (ret) {
    
    
         fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
         exit(-1);
     }

     /* 等待线程结束 */
     ret = pthread_join(tid1, NULL);
     if (ret) {
    
    
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
         exit(-1);
     }

     ret = pthread_join(tid2, NULL);
     if (ret) {
    
    
         fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
         exit(-1);
     }

     /* 打印结果 */
     printf("g_count = %d\n", g_count);
     exit(0);
 }

Compile the code, test it, execute the code first, and pass in the parameter 1000, that is, let each thread increment the global variable g_count 1000 times, as shown below:

sundp:~/codetest$ gcc test.c -lpthread
sundp:~/codetest$ ./a.out 1000
g_count = 2000

Print the results and see the results we imagined. Each thread increments 1000 times, and the final value is 2000; then we increase the number of increments, using the default value of 10 million times, as shown below

sundp:~/codetest$ gcc test.c -lpthread
sundp:~/codetest$ ./a.out
g_count = 10459972

It can be found that the result is not what we want to see. At the end of the execution, it should be 20 million. In fact, the problem shown in Figure 1.1 appears here, and the data is inconsistent.

How to solve the problem of data inconsistency in concurrent access to shared resources?

In order to solve the problem of data inconsistency in Figure 1.1, some methods provided by Linux, that is, thread synchronization technology, are needed to allow only one thread to access the variable at the same time, prevent concurrent access, and eliminate data inconsistency.

Figure 1.2 describes this synchronization operation. It can be seen from the figure that neither thread A nor thread B will access this variable at the same time. When thread A needs to modify the value of the variable, it must wait until the write operation is completed (it cannot be interrupted) ), before running thread B to read.

The main advantage of threads lies in the sharing of resources, such as information sharing through global variables. However, this convenient sharing comes at a price. It must be ensured that multiple threads will not modify the same variable at the same time, or that a thread will not read variables that are being modified by other threads. concurrent access. The Linux system provides a variety of mechanisms for thread synchronization. Common methods include: mutex locks, condition variables, spin locks, and read-write locks, etc.

2 mutexes

A mutex (mutex) is also called a mutex, which is essentially a lock. The mutex is locked before accessing shared resources, and the mutex is released (unlocked) after the access is completed ; the mutex After locking, any other thread that tries to lock the mutex again will be blocked until the current thread releases the mutex. If more than one thread is blocked when the mutex is released, then these blocked threads will be woken up, and they will all try to lock the mutex. When one thread successfully locks the mutex, other threads cannot It is locked again, and it can only be blocked again, waiting for the next unlock.

To give a very simple and easy-to-understand example, take the bathroom (shared resource) as an example, when a person (thread) comes and sees that there is no one in the bathroom, then it goes in and locks the door from the inside (locking the mutex) ); at this time two more people (threads) came, and they also wanted to enter the bathroom for convenience, but at this time the door could not be opened (the mutex failed to lock), because there were people inside, so they could only wait at this time (trapped Blocking); when the convenience of the people inside is over (access to shared resources is complete), open the lock (mutex lock unlock) and come out from inside. At this time, there are two people waiting outside. Reject the lock to lock), naturally two people can only go in one, the person who goes in locks the door again, and the other person can only continue to wait for it to come out.

In our programming, only when all threads accessing shared resources are designed with the same data access rules, the mutex can work normally. If one of the threads is allowed to access the shared resource without obtaining the lock, even if other threads apply for the lock before using the shared resource, data inconsistency will still occur.

The mutex uses pthread_mutex_tthe data type to indicate that before using the mutex, it must be initialized first, and the mutex can be initialized in two ways.

2.1 Mutex initialization

1. Use the PTHREAD_MUTEX_INITIALIZER macro to initialize the mutex

The mutex pthread_mutex_tis represented by the data type, pthread_mutex_twhich is actually a structure type, and the macro PTHREAD_MUTEX_INITIALIZERis actually a package for the structure assignment operation, as shown below:

#define PTHREAD_MUTEX_INITIALIZER
{ { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }

So it can be seen that the operation of using PTHREAD_MUTEX_INITIALIZERthe macro to initialize the mutex is as follows:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

The PTHREAD_MUTEX_INITIALIZER macro already carries the default attributes of a mutex.

2. Use the pthread_mutex_init() function to initialize the mutex

The use of PTHREAD_MUTEX_INITIALIZERmacros is only suitable for direct initialization at the time of definition. This method cannot be used for other situations, such as defining a mutex first and then initializing it, or a mutex dynamically allocated in the heap, such as using malloc () function to apply for the mutex object allocated, then in these cases, you can use the pthread_mutex_init() function to initialize the mutex, and
its function prototype is as follows:

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

The header file needs to be included to use this function <pthread.h>.

Function parameters and return values ​​have the following meanings:

mutex: The parameter mutex is a pthread_mutex_ttype pointer, pointing to the mutex object that needs to be initialized;
attr: The parameter attr is a pthread_mutexattr_ttype pointer, pointing to a pthread_mutexattr_ttype object, which is used to define the attributes of the mutex, if the parameter attr is set If it is NULL, it means that the attribute of the mutex is set to the default value. In this case, it is equivalent to PTHREAD_MUTEX_INITIALIZERinitializing in this way, but the difference is that the use of macros does not perform error checking.
Return value: return 0 if successful; return a non-zero error code if failed.

Tips: Note that when "man 3 pthread_mutex_init"the command is executed under the Ubuntu system, it prompts that the function cannot be found. It is not that the function does not exist under Linux, but that the man manual help information related to the function has not been installed. At this time, we only need to execute the installation "sudo apt-get install manpages-posix-dev". .

Example of using pthread_mutex_init()a function to initialize a mutex:

pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);

or:

pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(mutex, NULL);

2.2 Mutex locking and unlocking

After the mutex is initialized, it is in an unlocked state. The calling function pthread_mutex_lock()can lock and acquire the mutex, and the calling function pthread_mutex_unlock()can unlock and release the mutex. Its function prototype is as follows:

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

The use of these functions needs to include the header file <pthread.h>, the parameter mutex points to the mutex object; pthread_mutex_lock()and pthread_mutex_unlock()returns 0 when the call is successful; failure will return a non-zero error code.

Call pthread_mutex_lock()the function to lock the mutex. If the mutex is unlocked, the call will be successfully locked, and the function call will return immediately; if the mutex has been locked by other threads at this time, the call pthread_mutex_lock()will Blocks until the mutex is unlocked, at which point the call locks the mutex and returns.

Call pthread_mutex_unlock()the function to unlock the mutex that is already locked. It is wrong to:

Unlocks a mutex that is in an unlocked state;
unlocks a mutex locked by another thread.

If there are multiple threads in a blocked state waiting for the mutex to be unlocked, when the mutex is pthread_mutex_unlock()unlocked by the thread calling the function that currently locks it, these waiting threads will have the opportunity to lock the mutex, but it is impossible to determine which one The thread will do what it wants!

Example of use

Modify the sample code 1.1 by using a mutex. After modification, as shown in the sample code, a mutex is used to protect the access to the global variable g_count.

//示例代码 2.1 
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static int loops;
static int g_count = 0;
static pthread_mutex_t mutex;

static void *new_thread_start(void *arg)
{
    
    
    int loops = *((int *)arg);
    int l_count, j;
    for (j = 0; j < loops; j++) {
    
    
        pthread_mutex_lock(&mutex); //互斥锁上锁
 
        l_count = g_count;
        l_count++;
        g_count = l_count;
        
        pthread_mutex_unlock(&mutex);//互斥锁解锁
    }
    return (void *)0;
}

int main(int argc, char *argv[])
{
    
    
    pthread_t tid1, tid2;
    int ret;

    /* 获取用户传递的参数 */
    if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
    else
        loops = atoi(argv[1]);

    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);

    /* 创建 2 个新线程 */
    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret) {
    
    
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret) {
    
    
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret) {
    
    
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_join(tid2, NULL);
    if (ret) {
    
    
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 打印结果 */
    printf("g_count = %d\n", g_count);
    exit(0);
}

By default, it runs 1000w times, and the result is as follows:

sundp:~/codetest$ gcc test.c -lpthread
sundp:~/codetest$ ./a.out
g_count = 20000000

It can be seen that the correct result we want to see is indeed obtained, and g_countthe accumulation of each time can always be correct, but in the process of running the program, it is obvious that the time consumed by the lock will be relatively long, which involves performance problem , which will be introduced later.

2.3 pthread_mutex_trylock() function

When the mutex has been locked by other threads, the calling pthread_mutex_lock()function will be blocked until the mutex is unlocked; if the thread does not want to be blocked, you can use pthread_mutex_trylock()the function; the calling pthread_mutex_trylock()function tries to lock the mutex, if the mutex is unlocked, the call pthread_mutex_trylock()willlock the mutex and return immediately, if the mutex has been locked by other threads, calling pthread_mutex_trylock() fails to lock, but it will not block, but return error code EBUSY.

Its function prototype is as follows:

#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);

The parameter mutex points to the target mutex, and returns 0 on success, and returns a non-zero error code on failure, and returns EBUSY if the target mutex has been locked by other threads.

Example of use
Modify the sample code 2.1, using pthread_mutex_trylock()the replacement pthread_mutex_lock().

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

static int loops;
static int g_count = 0;
static pthread_mutex_t mutex;

static void *new_thread_start(void *arg)
{
    
    
    int loops = *((int *)arg);
    int l_count, j;
    for (j = 0; j < loops; j++) {
    
    
        while(pthread_mutex_trylock(&mutex));
 
        l_count = g_count;
        l_count++;
        g_count = l_count;
        //printf("tid is %ld g_count is %d \n",pthread_self(),g_count);
        pthread_mutex_unlock(&mutex);//互斥锁解锁
    }
    return (void *)0;
}

int main(int argc, char *argv[])
{
    
    
    pthread_t tid1, tid2;
    int ret;

    /* 获取用户传递的参数 */
    if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
    else
        loops = atoi(argv[1]);

    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);

    /* 创建 2 个新线程 */
    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret) {
    
    
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret) {
    
    
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret) {
    
    
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_join(tid2, NULL);
    if (ret) {
    
    
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 打印结果 */
    printf("g_count = %d\n", g_count);
    exit(0);
}

The entire execution result pthread_mutex_lock()is the same as the usage effect, and you can test it yourself.

2.4 Destroy the mutex

When the mutex is no longer needed, it should be destroyed by calling pthread_mutex_destroy()a function to destroy the mutex, whose function prototype is as follows:

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);

To use this function, the header file needs to be included <pthread.h>, and the parameter mutex points to the target mutex; it also returns 0 if the call is successful, and returns a non-zero error code if it fails.

A mutex that has not been unlocked cannot be destroyed, otherwise an error will occur;
a mutex that has not been initialized cannot be destroyed either.

After the mutex is pthread_mutex_destroy()destroyed, it can no longer be locked and unlocked. It needs to be called again pthread_mutex_init()to initialize the mutex before it can be used.

Example of use

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

static int loops;
static int g_count = 0;
static pthread_mutex_t mutex;

static void *new_thread_start(void *arg)
{
    
    
    int loops = *((int *)arg);
    int l_count, j;
    for (j = 0; j < loops; j++) {
    
    
        while(pthread_mutex_trylock(&mutex));
 
        l_count = g_count;
        l_count++;
        g_count = l_count;
        //printf("tid is %ld g_count is %d \n",pthread_self(),g_count);
        pthread_mutex_unlock(&mutex);//互斥锁解锁
    }
    return (void *)0;
}

int main(int argc, char *argv[])
{
    
    
    pthread_t tid1, tid2;
    int ret;

    /* 获取用户传递的参数 */
    if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
    else
        loops = atoi(argv[1]);

    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);

    /* 创建 2 个新线程 */
    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret) {
    
    
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret) {
    
    
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret) {
    
    
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_join(tid2, NULL);
    if (ret) {
    
    
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 打印结果 */
    printf("g_count = %d\n", g_count);
    exit(0);

    /* 销毁互斥锁 */
    pthread_mutex_destroy(&mutex);
    exit(0);
}

2.5 Mutex deadlock

Just imagine,If a thread tries to lock the same mutex twice, what happens? the situation isThe thread will fall into a deadlock state and will be blocked forever; This is a situation where deadlock occurs. In addition, there are many other ways to use mutexes that can also cause deadlock.

Sometimes, a thread needs to access two or more different shared resources at the same time, and each resource is managed by a different mutex.Deadlock can occur when more than one thread locks the same set of mutexes (two or more mutexes); For example, if more than one mutex is used in the program, if a thread is allowed to hold the first mutex all the time, and is blocked when trying to lock the second mutex, but owns the second mutex The thread is also trying to lock the first mutex. Because both threads are requesting resources owned by the other thread , neither thread can move forward and will be blocked forever, so a deadlock occurs. As shown in the sample code below:

// 线程 A
pthread_mutex_lock(mutex1);
pthread_mutex_lock(mutex2);
// 线程 B
pthread_mutex_lock(mutex2);
pthread_mutex_lock(mutex1);

This is like the mutual inclusion relationship between two header files in C language , then it will definitely report an error when compiling!

In our program, if multiple mutexes are used, the easiest way to avoid such deadlocks is to define the hierarchical relationship of mutexes . When multiple threads operate on a set of mutexes, The set of mutexes should always be locked in the same order. For example, in the above scenario, if two threads always lock mutex1 first and then lock mutex2, deadlock will not occur. Sometimes, the logic of the hierarchical relationship between mutexes is not clear enough. Even so, it is still possible to design a mandatory hierarchical order that all threads must follow.

But sometimes, the structure of the application program makes it difficult to sort the mutexes . The program is complex, and there are many mutexes and shared resources involved. It is really impossible for the program design to sort a group of mutexes in the same order. If the lock is locked, another method must be used.

For example, pthread_mutex_trylock()try to lock the mutex in a non-blocking way. In this scheme, the thread first uses the function pthread_mutex_lock()to lock the first mutex, and then uses the function pthread_mutex_trylock()to lock the remaining mutexes. If any pthread_mutex_trylock()call fails (returns EBUSY), then the thread releases all mutexes and can try again from the beginning after some time has elapsed. Compared with the first method of avoiding deadlock according to the hierarchical relationship, this method is less efficient because it may need to go through multiple loops.

There are still many ways to solve the problem of mutex deadlock, and the author has not studied it in detail. When you need to use this knowledge in actual programming applications, you can consult relevant materials and books for learning.

Use example
After thinking for a long time, there is no better example, so let's take a break for now!

2.6 Properties of mutexes

As mentioned above, pthread_mutex_init()the attribute of the mutex can be set when the function is called to initialize the mutex, which is specified by the parameter attr.

The parameter attr points to a pthread_mutexattr_ttype object, which defines the properties of the mutex . Of course, if the parameter attr is set to NULL, it means that the mutex properties are set to default values. About the properties of the mutex I am not going to discuss the details of the mutex properties in depth, nor will I pthread_mutexattr_tlist the properties defined in the type one by one. If the default attribute is not used, when calling pthread_mutex_init()the function, the parameter attr must point to an pthread_mutexattr_tobject instead of NULL. After defining pthread_mutexattr_tan object, you need to use pthread_mutexattr_init()a function to initialize the object. When the object is no longer used, you need to use pthread_mutexattr_destroy()it to destroy it. The function prototype is as follows:

#include <pthread.h>
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
int pthread_mutexattr_init(pthread_mutexattr_t *attr);

The parameter attr points to the object that needs to be initialized pthread_mutexattr_t. If the call succeeds, it will return 0, and if it fails, it will return a non-zero error code.

pthread_mutexattr_init()function will use the defaultmutex attributeInitialize the pthread_mutexattr_t object pointed to by the parameter attr.
There are many attributes about mutexes, such as process sharing attributes , robustness attributes , type attributes, etc., and I will not introduce them one by one here. This section discusses the type attributes, and the others will not be explained for the time being.

The type attribute of the mutex controls the locking characteristics of the mutex, and there are 4 types in total:

PTHREAD_MUTEX_NORMAL: A standard mutex type that does not do any error checking or deadlock detection. A deadlock occurs if a thread attempts to re-lock a mutex that has already been locked by itself; unlocking the mutex, which is unlocked or already locked by another thread, results in undefined results.

PTHREAD_MUTEX_ERRORCHECK: This type of mutex provides error checking. For example, these three situations will cause an error to be returned:A thread tries to lock a mutex already locked by itself(The same thread locks the same mutex twice), returns an error;A thread unlocks a mutex locked by another thread, returns an error;The thread unlocks the mutex in the unlocked state, returns an error. This type of mutex is slower to run because it requires error checking, but it can be used as a debugging tool to find where the program violates the basic principles of mutex use.
PTHREAD_MUTEX_RECURSIVE: This type of mutex allows the same thread to lock the mutex multiple times before the mutex is unlocked, and then maintains the number of times the mutex is locked. This type of mutex is called a recursive mutex, but If the number of unlocks is not equal to the number of accelerations, the lock will not be released; therefore, if a recursive mutex is locked twice and then unlocked once, then the mutex is still in the locked state. Before unlocking it again The lock will not be released.
PTHREAD_MUTEX_DEFAULT: This type of mutex provides default behavior and features. 使用宏PTHREAD_MUTEX_INITIALIZERInitialized mutexes, or pthread_mutexattr_init()mutexes created by calling a function with parameter arg set to NULL, all belong to this type. This type of lock is intended to preserve maximum flexibility in the implementation of the mutex. On Linux, the behavior of the PTHREAD_MUTEX_DEFAULT type mutex is similar to the PTHREAD_MUTEX_NORMAL type.

You can use pthread_mutexattr_gettype()the function to get the type attribute of the mutex, and use pthread_mutexattr_settype()modify to set the type attribute of the mutex. The function prototype is as follows:

#include <pthread.h>
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

To use these functions, you need to include the header file <pthread.h>, and the parameter attr points to pthread_mutexattr_tthe type object; for pthread_mutexattr_gettype()the function, the mutex type attribute will be saved in the memory pointed to by the parameter type if the function is called successfully, and it will be returned through it; and for the function, the pthread_mutexattr_settype()parameter attr will be The type property of the object pointed to pthread_mutexattr_tis set to the type specified by the parameter type. The usage is as follows:

pthread_mutex_t mutex;
pthread_mutexattr_t attr;
/* 初始化互斥锁属性对象 */
pthread_mutexattr_init(&attr);
/* 将类型属性设置为 PTHREAD_MUTEX_NORMAL */
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL);
/* 初始化互斥锁 */
pthread_mutex_init(&mutex, &attr);
......
/* 使用完之后 */
pthread_mutexattr_destroy(&attr);
pthread_mutex_destroy(&mutex);

3 Condition variables

This section discusses the second method of thread synchronization—condition variables.

Condition variables are another synchronization mechanism available to threads. Condition variables are used to automatically block threads until a specific event occurs or a condition is met . Usually, condition variables are used in conjunction with mutexes. Using condition variables mainly includes two actions:

One thread is blocked waiting for a certain condition to be met;
in another thread, a "signal" is sent when the condition is met.

To illustrate this problem, let's look at an example without using condition variables, the producer-consumer model, the producer is responsible for producing products, and the consumer is responsible for consuming products. For consumers, when there is no product, they can only wait. When the product comes out, use it when there is a product.

Here we use a variable to represent this product. The producer produces a product variable plus 1, and the consumer consumes a variable minus 1. The sample code is as follows:

//3.1 生产者---消费者示例代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static pthread_mutex_t mutex;
static int g_avail = 0;

/* 消费者线程 */
static void *consumer_thread(void *arg)
{
    for ( ; ; ) {
        pthread_mutex_lock(&mutex);//上锁
        while (g_avail > 0)
        g_avail--; //消费
        pthread_mutex_unlock(&mutex);//解锁
    }
    return (void *)0;
}

/* 主线程(生产者) */
int main(int argc, char *argv[])
{
    pthread_t tid;
    int ret;
    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, consumer_thread, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    for ( ; ; ) {
        pthread_mutex_lock(&mutex);//上锁
        g_avail++; //生产
        pthread_mutex_unlock(&mutex);//解锁
    }
    exit(0);
}

In this code, the main thread acts as a "producer", and the newly created thread acts as a "consumer". After running, they are all in an infinite loop, so the code does not add codes related to destroying mutexes and waiting for new threads to be recycled. It is automatically handled when the process terminates.

The above code works, but due toThe new thread will continuously check whether the global variable g_avail is greater than 0, thus causing a waste of CPU resources. This problem can be easily solved by using condition variables ! The condition variable allows a thread to sleep (blocking and waiting) until it is notified by another thread (receiving a signal) before performing its own operations . For example, in the above code, when the condition g_avail > 0 is not true, the consumer thread will enter the sleep state , and after the producer generates the product (g_avail++, g_avail will be greater than 0 at this time), it sends a "signal" to the thread in the waiting state, and other threads will wake up after receiving the "signal"!

As mentioned earlier,Condition variables are usually used with mutex locks, because the detection of the condition is carried out under the protection of the mutex, that is to say, the condition itself is protected by the mutex, and the thread must first lock the mutex before changing the state of the condition, otherwise it may cause the thread to fail. safety issue.

3.1 Condition variable initialization

A condition variable is pthread_cond_trepresented using a data type, similar to a mutex, which must be initialized before it can be used. There are also two initialization methods: using macros PTHREAD_COND_INITIALIZERor using functions pthread_cond_init(). The initialization method using macros is the same as that of mutexes, so I won’t repeat them here!
for example:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

pthread_cond_init()The function prototype looks like this:

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);

Also, using these functions requires including the header file <pthread.h>, using pthread_cond_init()the function to initialize the condition variable, and using it to destroy the condition variable when it is no longer used pthread_cond_destroy().

The parameter condpoints to pthread_cond_tthe condition variable object. For pthread_cond_init()a function, it is similar to a mutex. When the condition variable is initialized, the property of the condition variable is set. The parameter attrpoints to a pthread_condattr_ttype object, and pthread_condattr_tthe data type is used to describe the property of the condition variable. The parameter can attrbe set to NULL, indicating that the default value of the attribute is used to initialize the condition variable, the PTHREAD_COND_INITIALIZERsame as using a macro.

The function call returns 0 successfully, and returns a non-zero error code if it fails.

For initialization and destruction operations, the following issues need attention:

Before using the condition variable, the condition variable must be initialized , either by using PTHREAD_COND_INITIALIZERa macro or a function ; re-initializing the initialized condition variable may lead to undefined behavior; destroying the uninitialized condition variable will also cause May lead to undefined behavior; for a condition variable, it is safest to destroy it only when no thread is waiting for it; a condition variable destroyed by pthread_cond_destroy() can be called pthread_cond_init() again to reinitialize.pthread_cond_init()



3.2 Notify and wait for condition variables

The main operations of condition variables are sending signals (signal) and waiting . The operation of sending a signal is to notify one or more threads in the waiting state that the state of a shared variable has changed. These threads in the waiting state will be woken up after receiving the notification, and then check whether the conditions are met after waking up. A wait operation is one that remains blocked until a notification is received.

Both function pthread_cond_signal()and pthread_cond_broadcast()can send a signal to the specified condition variable, notifying one or more waiting threads. The calling pthread_cond_wait()function is the thread blocking until it is notified of the condition variable.

pthread_cond_signal()and pthread_cond_broadcast()the function prototype looks like this:

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

Using these functions requires including the header file <pthread.h>, and the parameter condpoints to the target condition variable to send the signal to. If the call succeeds, it will return 0; if it fails, it will return a non-zero error code.

pthread_cond_signal()pthread_cond_broadcast()The difference between and is that they pthread_cond_wait()deal with multiple threads blocked in different ways. pthread_cond_signal()The function can wake up at least one thread, while pthread_cond_broadcast()the function can wake up all threads. Using pthread_cond_broadcast()a function always produces the correct result, waking up all waiting threads, but the function pthread_cond_signal()is more efficient because it only needs to ensure that at least one thread is woken up, so if there is only one thread in the waiting state in our program, It is better to use pthread_cond_signal(), which function to use is selected according to the actual situation!

pthread_cond_wait()The function prototype looks like this:

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

When a condition variable is used in the program, when it is judged that a certain condition is not met, the calling pthread_cond_wait()function sets the thread to the waiting state (blocked). pthread_cond_wait()The function takes two parameters:

cond: Points to the condition variable that needs to wait, the target condition variable;
mutex: The parameter mutex is a pthread_mutex_t type pointer, pointing to a mutex object;

As I introduced at the beginning, condition variables are usually used together with mutexes, because condition detection (condition detection usually requires access to shared resources) is carried out under the protection of mutexes, that is to say, the conditions themselves is protected by a mutex.

Return value: 0 is returned if the call succeeds; a non-zero error code is returned if it fails.

Inside pthread_cond_wait()the function mutex, the mutex specified by the parameter will be operated. Usually, the condition judgment and pthread_cond_wait()function call are protected by the mutex, that is to say, the thread has locked the mutex before this. When calling pthread_cond_wait()a function, the caller passes the mutex to the function, and the function will automatically put the calling thread on the thread list waiting for the condition, and then unlock the mutex; when it is woken up and pthread_cond_wait()returns, it will lock the mutex again.

Note
Note that condition variables do not hold state information, but are just a communication mechanism for passing application state information. If there are no threads waiting on the condition variable when calling pthread_cond_signal()and pthread_cond_broadcast()sending a signal to the specified condition variable, the signal will be ignored.
When pthread_cond_broadcast() is called to wake up all threads at the same time, the mutex can only be locked by a certain thread, and other threads will be blocked if they fail to acquire the lock.

Use the example
to modify the sample code 3.1 using condition variables. When the consumer thread has no products to consume, let it wait until the producer produces the product; when the producer produces the product, it will notify the consumer. .

//3.1 生产者---消费者示例代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

static pthread_mutex_t mutex; //定义互斥锁
static pthread_cond_t cond; //定义条件变量
static int g_avail = 0; //全局共享资源


/* 消费者线程 */
static void *consumer_thread(void *arg)
{
    for ( ; ; ) {
        pthread_mutex_lock(&mutex);//上锁
        while (0 >= g_avail)
            pthread_cond_wait(&cond, &mutex);//等待条件满足
        while (0 < g_avail)
        {
            g_avail--; //消费
            printf("消费:tid is %ld , g_avail = %d\n",pthread_self(),g_avail);
        }
        pthread_mutex_unlock(&mutex);//解锁
    }
    return (void *)0;
}

/* 主线程(生产者) */
int main(int argc, char *argv[])
{
    pthread_t tid;
    int ret;
    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);
    /* 初始化条件变量 */
    pthread_cond_init(&cond, NULL);
    /* 创建新线程 */
    ret = pthread_create(&tid, NULL, consumer_thread, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    for ( ; ; ) {
        pthread_mutex_lock(&mutex);//上锁
        g_avail++; //生产
        printf("生产:tid is %ld , g_avail = %d\n",pthread_self(),g_avail);
        pthread_mutex_unlock(&mutex);//解锁
        pthread_cond_signal(&cond);//向条件变量发送信号
    }
    exit(0);
}

Global variables g_availare shared resources between the main thread and new threads,Two threads first lock the mutex between accessing them, in the consumer thread, when it is judged that there is no product to be consumed (g_avail <= 0), the call pthread_cond_wait()causes the thread to fall into a waiting state, wait for the condition variable, and wait for the producer to manufacture the product; after the call, pthread_cond_wait()the thread blocks and unlocks the mutex; and in In the producer thread, its task is to produce products (using g_avail++ to simulate), after the product production is completed, call to pthread_mutex_unlock()unlock the mutex, and call to pthread_cond_signal()send a signal to the condition variable;This will wake up the consumer threads that are waiting on the condition variable, automatically acquire the mutex again after waking up, and then consume the product (g_avai-simulation).

3.3 Judgment conditions of condition variables

When using condition variables, there will be related judgment conditions. Usually, one or more shared variables will be involved. For example, in the sample code 3.2, the judgment related to the condition variable is (0 >= g_avail). Attentive readers will find that in this sample code, we use a while loop instead of an if statement to control the call to pthread_cond_wait(), why?

A while loop must be used instead of an if statement. This is a general design principle: when the thread returns from the pthread_cond_wait()thread, the state of the judgment condition cannot be determined, and the judgment condition should be rechecked immediately. If the condition is not satisfied, then continue to sleep and wait .

After pthread_cond_wait()returning, it is not possible to determine whether the judgment condition is true or false. The reasons are as follows:

When more than one thread is waiting for the condition variable, any thread may wake up first to acquire the mutex, and the thread that wakes up first to acquire the mutex may modify the shared variable, thereby changing the state of the judgment condition . For example, in sample code 3.2, if there are two or more consumer threads, when one of the consumer threads pthread_cond_wait()returns from the thread, it will change the value of the global shared variable g_avail to 0, causing the status of the judgment condition to change from true to false .

False notifications may be issued.

In providing a case, but it will run wrong, look back:

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

//节点结构体
struct msg
{
    
    
    int num; //数据区
    struct msg *next; //链表区
};

struct msg *head = NULL;//头指针
struct msg *mp = NULL;  //节点指针

//利用宏定义的方式初始化全局的互斥锁和条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;

void *producter(void *arg)
{
    
    
    while (1)
    {
    
    
        mp = (struct msg *)malloc(sizeof(struct msg));
        mp->num = rand() % 400 + 1;
        printf("producer: %d\n", mp->num);
        
        pthread_mutex_lock(&mutex);//访问共享区域必须加锁
        mp->next = head;
        head = mp;
        pthread_mutex_unlock(&mutex);
        
        pthread_cond_signal(&has_product);//通知消费者来消费
        
        sleep(rand() % 3);
    }
    
    return NULL;
}

void *consumer(void *arg)
{
    
    
    while (1)
    {
    
    
        pthread_mutex_lock(&mutex);//访问共享区域必须加锁
        while (head == NULL)//如果共享区域没有数据,则解锁并等待条件变量
        {
    
    
            pthread_cond_wait(&has_product, &mutex);
        }
        mp = head;
        head = mp->next;
        pthread_mutex_unlock(&mutex);
        
        printf("consumer: %d\n", mp->num);
        free(mp); //释放被删除的节点内存
        mp = NULL;//并将删除的节点指针指向NULL,防止野指针
        
        sleep(rand() % 3);
    }
    
    return NULL;
}

int main(void)
{
    
    
    pthread_t ptid, ctid;
    
    //创建生产者和消费者线程
    pthread_create(&ptid, NULL, producter, NULL);
    pthread_create(&ctid, NULL, consumer, NULL);
    //主线程回收两个子线程
    pthread_join(ptid, NULL);
    pthread_join(ctid, NULL);
    
    return 0;
}

3.4 Properties of condition variables

As mentioned above, pthread_cond_init()when the function is called to initialize the condition variable, the attribute of the condition variable can be set, which is specified by the parameter attr. The parameter attr points to a pthread_condattr_ttype object, which defines the properties of the condition variable. Of course, if the parameter attr is set to NULL, it means that the default value is used to initialize the condition variable properties.

The properties of condition variables are not going to be discussed in depth here. Condition variables include two properties: process sharing properties and clock properties . Each attribute provides the corresponding get method and set method. If readers are interested, you can refer to the information and learn by yourself, so I won’t introduce it here!

4 spin locks

The spin lock is very similar to the mutex lock, and it is also a lock in essence. The spin lock is locked before accessing the shared resource, and the spin lock is released (unlocked) after the access is completed; in fact, from the implementation Generally speaking, mutexes are implemented based on spin locks, so spin locks are lower than mutexes.

If the spin lock is unlocked when the spin lock is acquired, the lock will be acquired immediately (lock the spin lock); if the spin lock is already locked when the spin lock is acquired, then the lock will be acquired The operation will "spin" in place until the spinlock holder releases the lock .

From this introduction, we can see that the spin lock is similar to the mutex lock, but the mutex lock will make the thread fall into a blocked waiting state when the lock cannot be obtained; while the spin lock will "spin" in place when the lock cannot be obtained "wait. "Spin" actually means that the caller has been looping to see if the holder of the spin lock has released the lock, hence the name "spin".

The disadvantage of the spin lock is that the CPU that the spin lock has been occupying has been running (spinning) without obtaining the lock, so it occupies the CPU. If the lock cannot be obtained in a short time , which will undoubtedly make the CPU less efficient.

Trying to lock the same spinlock twice will inevitably lead to deadlock , and trying to lock the same mutex twice will not necessarily cause deadlock, because mutexes have different types. When set PTHREAD_MUTEX_ERRORCHECK类型时to Error checking, the second lock will return an error, so it will not enter the deadlock state.

Therefore, we should use spin locks carefully. Spin locks are usually used in the following situations: the execution time of the code segment that needs to be protected is very short, so that the thread holding the lock will release the lock quickly, and the "spin" waiting The thread also only needs to wait for a short time; in this case, it is more suitable to use a spin lock, which is efficient!

To sum up, let's summarize the difference between spin locks and mutexes:

  1. Differences in implementation methods: Mutex locks are implemented based on spin locks, so spin locks are lower-level than mutex locks;
  2. The difference in overhead: if the mutex cannot be obtained, it will fall into a blocked state (sleep), until it is awakened when the lock is obtained; if the spin lock cannot be obtained, it will "spin" in place until the lock is obtained; sleep and wake up The overhead is very high, so the overhead of the mutex is much higher than that of the spin lock, and the efficiency of the spin lock is much higher than that of the mutex; but if the "spin" waits for a long time, the CPU usage efficiency will be reduced , so spin locks are not suitable for situations where the waiting time is relatively long.
  3. Differences in usage scenarios: Spin locks are rarely used in user-mode applications, and are usually used more in kernel code; because spin locks can be used in interrupt service functions , but mutexes cannot be used in execution interrupts. When serving functions, it is required not to sleep or be preempted (the use of spin locks in the kernel will automatically prohibit preemption). Once dormant, it means that the CPU usage right is actively handed over when the interrupt service function is executed, and it cannot return to the interrupt service function when the sleep is over. , which would lead to a deadlock!

4.1 Spinlock initialization

The spin lock is pthread_spinlock_trepresented by the data type. After the spin lock is defined, pthread_spin_init()it needs to be initialized with a function. When the spin lock is no longer used, pthread_spin_destroy()the function is called to destroy it. The function prototype is as follows:

#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

Using these two functions requires including the header file <pthread.h>. The parameter lock points to the spin lock object that needs to be initialized or destroyed, and the parameter pshared indicates the process shared attribute of the spin lock, which can
take the following values:

PTHREAD_PROCESS_SHARED: shared spin lock. The spin lock can be shared between threads in multiple processes;
PTHREAD_PROCESS_PRIVATE: private spin lock. Only threads in this process can use the spin lock.

These two functions return 0 if the call is successful; failure will return a non-zero error code.

4.2 Spinlock locking and unlocking

You can use pthread_spin_lock()a function or pthread_spin_trylock()a function to lock the spin lock. The former keeps "spinning" when the lock is not acquired; for the latter, if the lock cannot be acquired, an error is returned immediately, and the error code is EBUSY. No matter how the lock is locked, the spin lock can use pthread_spin_unlock()the function to unlock the spin lock. Its function prototype is as follows:

#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

Use of these functions requires including the header file <pthread.h>. The parameter lock points to the spin lock object, and the call returns 0 if the call is successful, and a non-zero error code is returned if it fails.

If the spin lock is unlocked, the call pthread_spin_lock()will lock it (lock). If other threads have locked the spin lock, then this call will "spin" and wait; if you try to use the same spin lock Locking twice will inevitably lead to deadlock.

Example of use
Modify sample code 2.1, use spin lock to replace mutex to realize thread synchronization, and protect access to shared resources.

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

static int loops;
static int g_count = 0;
static pthread_spinlock_t spin;//定义自旋锁

static void *new_thread_start(void *arg)
{
    int loops = *((int *)arg);
    int l_count, j;
    for (j = 0; j < loops; j++) {
        pthread_spin_lock(&spin); //自旋锁上锁
 
        l_count = g_count;
        l_count++;
        g_count = l_count;
        
        pthread_spin_unlock(&spin);//自旋锁解锁
    }
    return (void *)0;
}

int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    int ret;

    /* 获取用户传递的参数 */
    if (2 > argc)
        loops = 10000000; //没有传递参数默认为 1000 万次
    else
        loops = atoi(argv[1]);

    /* 初始化自旋锁(私有) */
    pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE);

    /* 创建 2 个新线程 */
    ret = pthread_create(&tid1, NULL, new_thread_start, &loops);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_create(&tid2, NULL, new_thread_start, &loops);
    if (ret) {
        fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 等待线程结束 */
    ret = pthread_join(tid1, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }
    ret = pthread_join(tid2, NULL);
    if (ret) {
        fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
        exit(-1);
    }

    /* 打印结果 */
    printf("g_count = %d\n", g_count);
    exit(0);
}

operation result:

sundp@sundongpeng:~/workspace/IMX6ULL/Board_Drivers$ gcc test.c -lpthread
sundp@sundongpeng:~/workspace/IMX6ULL/Board_Drivers$ ./a.out
g_count = 20000000

After replacing the mutex with a spin lock, there is no problem in printing the test results, and through comparison, it can be found that after replacing it with a spin lock, the time it takes for the program to run is significantly shorter, indicating that the spin lock is indeed faster than the mutual exclusion lock. The repelling lock is more efficient, but we must pay attention to the scenarios where the spin lock is applicable.

5 read-write lock

A mutex or spinlock is either locked or unlocked, and only one thread can lock it at a time. Read-write locks have three states: locked state in read mode (hereinafter referred to as read-locked state), locked state in write mode (hereinafter referred to as write-locked state) and unlocked state (see ). A thread can hold a read-write lock in write mode, but multiple threads can hold read-write locks in read mode at the same time. Therefore, it can be seen that read-write locks have higher parallelism than mutex locks!

stateDiagram-v2
    state if_state <>
    [*] --> 读写锁
    读写锁  --> 读模式下的读写锁
    读写锁  --> 写模式下的读写锁
    读写锁  --> 不加锁

Read-write locks have the following two rules:

  1. When the read-write lock is in the write-locked state, all threads that attempt to lock the lock (whether locked in read mode or locked in write mode) will be blocked before the lock is unlocked.
  2. When the read-write lock is in the read-locked state, all threads that try to lock it in read mode can lock it successfully; but any thread that locks it in write mode will be blocked until all the threads holding the read lock are locked. Mode-locked threads release their locks.

Although each operating system implements the read-write lock differently, when the read-write lock is locked in read mode and a thread tries to acquire the lock in write mode, the thread will be blocked; and if another If the thread acquires the lock in read mode, it will successfully acquire the lock and perform read operations on the shared resource.

so,Read-write locks are very suitable for situations where the number of reads to shared data is much greater than the number of writes. When the read-write lock is locked in write mode, the data it protects can be safely modified, because only one thread can own the lock in write mode at a time; when the read-write lock is locked in read mode, it The protected data can then be read by multiple threads acquiring the read mode lock. Therefore, in the application program, use the read-write lock to realize thread synchronization. When the thread needs to read the shared data, it needs to acquire the read mode lock first (lock the read mode lock), and then release the read mode lock after the read operation is completed. Mode lock (unlock the read mode lock); when a thread needs to write to the shared data, it needs to acquire the write mode lock first, and then release the write mode lock after the write operation is completed.

Read-write locks are also called shared mutexes. When the read-write lock is locked in read mode, it can be said to be locked in shared mode. When it is locked in write mode, it can be said to be locked in exclusive mode.

5.1 Read-write lock initialization

Similar to mutual exclusion locks and spin locks, the read-write lock must be initialized before using the read-write lock. The read-write lock is represented by the pthread_rwlock_tdata type, and the initialization of the read-write lock can use macros PTHREAD_RWLOCK_INITIALIZERor functions pthread_rwlock_init(). Exclusive locks are the same. For example, when using macros PTHREAD_RWLOCK_INITIALIZERfor initialization, they must be initialized when defining read-write locks:

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

For other methods, you can use pthread_rwlock_init()a function to initialize it. When the read-write lock is no longer used, you need to call pthread_rwlock_destroy()the function to destroy it. The function prototype is as follows:

#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);

The use of these two functions also needs to include the header file <pthread.h>. If the call is successful, it will return 0, and if it fails, it will return a non-zero error code.

The parameter rwlockpoints to the read-write lock object that needs to be initialized or destroyed. For pthread_rwlock_init()functions, the parameter attr is a pthread_rwlockattr_t *type pointer, pointing to pthread_rwlockattr_tthe object. pthread_rwlockattr_tThe data type defines the attributes of the read-write lock. If the parameter attr is set to NULL, it means that the attributes of the read-write lock are set to the default value. In this case, it is equivalent to initializing in this way, but the PTHREAD_RWLOCK_INITIALIZERdifference The catch is that using macros does not do error checking.

When the read-write lock is no longer used, pthread_rwlock_destroy()a function needs to be called to destroy it.

Example of using read-write lock initialization:

pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);

pthread_rwlock_destroy(&rwlock);

5.2 Locking and unlocking the read-write lock

To lock a read-write lock in read mode, you need to call pthread_rwlock_rdlock()a function; to lock a read-write lock in write mode, you need to
call pthread_rwlock_wrlock()a function. No matter how the read-write lock is locked, pthread_rwlock_unlock()the function can be called to unlock it. The function prototype is as follows:

#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

To use these functions, you need to include the header file <pthread.h>, and the parameter rwlock points to the read-write lock object. If the call succeeds, it returns 0, and if it fails, it returns a
non-zero error code.

When the read-write lock is in the lock state of the write mode, other thread calls pthread_rwlock_rdlock()or pthread_rwlock_wrlock()functions will fail to acquire the lock, thus falling into a blocked waiting state; when the read-write lock is in the lock state of the read mode, other threads pthread_rwlock_rdlock()can successfully obtain the lock by calling the function , if pthread_rwlock_wrlock()the function is called, the lock cannot be obtained, thus falling into a blocked waiting state.

If the thread does not want to be blocked, it can call pthread_rwlock_tryrdlock()and pthread_rwlock_trywrlock()to try to lock, if the lock cannot be acquired. Both functions will immediately return an error with the error code EBUSY. Its function prototype is as follows:

#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

The parameter rwlock points to the read-write lock that needs to be locked. If the lock is successful, it will return 0, and if the lock fails, it will return EBUSY.

Example of use

Sample code 5.1 demonstrates the use of read-write locks to achieve thread synchronization. Global variables are g_countshared variables between threads. Five threads that read variables are created in the main thread . g_countThey use the same function . Read and print it out, together with the thread number (1~5); 5 threads for writing variables are also created in the main thread, they use the same function , the value of the variable will be accumulated in the function , and the loop 10 times, each time increase the value of the variable by 20 on the original basis, and print it out, together with the thread number (1~5).read_threadg_countg_countwrite_threadwrite_threadg_countg_count

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

static int nums[5] = {
    
    0, 1, 2, 3, 4};
static pthread_rwlock_t rwlock;//定义读写锁
static int g_count = 0;

static void *read_thread(void *arg)
{
    
    
    int number = *((int *)arg);
    int j;
    for (j = 0; j < 10; j++) {
    
    
        pthread_rwlock_rdlock(&rwlock); //以读模式获取锁
        printf("读线程<%d>, g_count=%d\n", number+1, g_count);
        pthread_rwlock_unlock(&rwlock);//解锁
        sleep(1);
    }
    return (void *)0;
}
static void *write_thread(void *arg)
{
    
    
    int number = *((int *)arg);
    int j;
    for (j = 0; j < 10; j++) {
    
    
        pthread_rwlock_wrlock(&rwlock); //以写模式获取锁
        printf("写线程<%d>, g_count=%d\n", number+1, g_count+=20);
        pthread_rwlock_unlock(&rwlock);//解锁
        sleep(1);
    }
    return (void *)0;
}

int main(int argc, char *argv[])
{
    
    
    pthread_t tid[10];
    int j;
    /* 对读写锁进行初始化 */
    pthread_rwlock_init(&rwlock, NULL);
    /* 创建 5 个读 g_count 变量的线程 */
    for (j = 0; j < 5; j++)
        pthread_create(&tid[j], NULL, read_thread, &nums[j]);
    /* 创建 5 个写 g_count 变量的线程 */
    for (j = 0; j < 5; j++)
        pthread_create(&tid[j+5], NULL, write_thread, &nums[j]);
    /* 等待线程结束 */
    for (j = 0; j < 10; j++)
        pthread_join(tid[j], NULL);//回收线程
    /* 销毁自旋锁 */
    pthread_rwlock_destroy(&rwlock);
    exit(0);
}

Compile the test, and the printed results are as follows:

sundp@sundongpeng:~/workspace/IMX6ULL/Board_Drivers$ gcc test.c -lpthread
sundp@sundongpeng:~/workspace/IMX6ULL/Board_Drivers$ ./a.out
read thread<1>, g_count=0
read thread<2>, g_count=0
read thread<5>, g_count=0
read thread<3>, g_count=0
read thread<4>, g_count=0
write thread<1>, g_count=20
write thread<2>, g_count=40
write thread <3>, g_count=60
write threads<5>, g_count=80
write threads<4>, g_count=100
write threads<2>, g_count=120
write threads<3>, g_count=140
read threads<5>, g_count =140
read threads<3>, g_count=140
read threads<1>, g_count=140
read threads<2>, g_count=140
read threads<4>, g_count=140
write threads<5>, g_count=160
write threads< 4>,g_count=180
write thread<1>, g_count=200
read thread<4>, g_count=200
Read thread<5>, g_count=200
read thread<1>, g_count=200
read thread<2>, g_count=200
read thread<3>, g_count=200
write thread<2>, g_count=220
write thread<3> , g_count=240
write threads<4>, g_count=260
write threads<5>, g_count=280
write threads<1>, g_count=300
read threads<3>, g_count=300
read threads<1>, g_count=300
read thread<5>, g_count=300
read thread<4>, g_count=300
read thread<2>, g_count=300

In this example, we demonstrate the use of read-write locks, but only for demonstration purposes. In actual application programming, you need to choose whether to use read-write locks according to the application scenario.

5.3 Properties of read-write locks

Read-write locks are similar to mutex locks, and also have attributes. The attributes of read-write locks are represented by pthread_rwlockattr_tdata types. When defining pthread_rwlockattr_tan object, you need to use pthread_rwlockattr_init()a function to initialize it. Initialization will pthread_rwlockattr_tdefine each read-write lock attribute defined by the object It is initialized to the default value; when pthread_rwlockattr_tthe object is no longer used , it needs to call pthread_rwlockattr_destroy()a function to destroy it, and its function prototype is as follows:

#include <pthread.h>
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);

The parameter attr points to the object that needs to be initialized or destroyed pthread_rwlockattr_t; the function call returns 0 successfully, and returns a non-zero error code if it fails.

The read-write lock has only one attribute, which is the process-shared attribute , which is the same as the process-shared attribute of the mutex and the spin lock. Corresponding functions are provided under Linux for setting or obtaining shared attributes of read-write locks. The function pthread_rwlockattr_getpshared() is used to pthread_rwlockattr_tobtain the shared property from the object, and the function pthread_rwlockattr_setpshared()is used to set pthread_rwlockattr_tthe shared property in the object, and its function prototype is as follows:

#include <pthread.h>
int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);

Function pthread_rwlockattr_getpshared()parameters and return values:

attr: points to the pthread_rwlockattr_t object;
pshared: calls pthread_rwlockattr_getpshared() to obtain the shared attribute, and saves it in the memory pointed to by the parameter pshared;
return value: returns 0 on success, and returns a non-zero error code on failure.

Function pthread_rwlockattr_setpshared()parameters and return values:

attr: Point to the pthread_rwlockattr_t object;
pshared: Call pthread_rwlockattr_setpshared() to set the shared attribute of the read-write lock, and set it to the value specified by the parameter pshared.
The possible values ​​of the parameter pshared are as follows:
PTHREAD_PROCESS_SHARED: shared read-write lock. The read-write lock can be shared between threads in multiple processes;
PTHREAD_PROCESS_PRIVATE: private read-write lock. Only threads in this process can use the read-write lock, which is the default value of the shared attribute of the read-write lock.
Return value: return 0 if the call is successful; return a non-zero error code if it fails.

The usage is as follows:

pthread_rwlock_t rwlock; //定义读写锁
pthread_rwlockattr_t attr; //定义读写锁属性
/* 初始化读写锁属性对象 */
pthread_rwlockattr_init(&attr);
/* 将进程共享属性设置为 PTHREAD_PROCESS_PRIVATE */
pthread_rwlockattr_setpshared(&attr, PTHREAD_PROCESS_PRIVATE);
/* 初始化读写锁 */
pthread_rwlock_init(&rwlock, &attr);
......
/* 使用完之后 */
pthread_rwlock_destroy(&rwlock); //销毁读写锁
pthread_rwlockattr_destroy(&attr); //销毁读写锁属性对象

Guess you like

Origin blog.csdn.net/weixin_42581177/article/details/129804400