[Operating system] Linux programming - Creation and use of multi-threads II (Usage of critical sections, mutexes, and semaphores)

        This article has a total of 4505 words, and the estimated reading time is 8 minutes.

Table of contents

The concept of critical section 

Simulate multi-threaded tasks

Thread synchronization

mutex

Mutex locking and unlocking

Signal amount


The concept of critical section 

        In the previous example, we only tried to create 1 thread to handle the task. Next, let us try to create multiple threads.

        However, we still need to expand a concept first - "critical zone"

        A critical section refers to a program fragment that accesses shared resources (such as shared devices or shared memory) , and these shared resources cannot be accessed by multiple threads at the same time. When a thread enters a critical section, other threads or processes must wait (for example: bounded waiting waiting method) . Some synchronization mechanisms must be implemented at the entry and exit points of the critical section to ensure that these shared resources are used. Mutex is obtained using, for example: semaphore .

        It is easy to cause problems when multiple threads execute at the same time. This problem focuses on access to shared resources.

        Depending on whether the function will cause problems in the critical section when executed, the types of functions can be divided into two categories:

  • Thread-safe function
  • Thread-unsafe function

        Thread-safe functions do not cause problems when called by multiple threads simultaneously , while non- thread-safe functions cause problems when called by multiple threads.

expand:

        Whether it is Linux or Windwos, we do not need to distinguish between thread-safe functions and non-thread-safe functions, because while designing non-thread-safe functions, developers also design thread-safe functions with the same functions.

        The names of thread-safe functions are generally added with the suffix _r in the function . However, if we all write function expressions in this way in programming, it will become very troublesome. For this reason, we can define it before declaring the header file. _REENTRANT macro.

        If you are pursuing a faster code writing experience, you can add -D_REENTRANT when compiling the input parameters instead of referencing the _REENTRANT macro when writing code .

Simulate multi-threaded tasks

        Next, let us simulate a scenario to reflect this problem. The following is sample code:

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

#define THREAD_NUM 100

void *thread_inc(void *arg);
void *thread_des(void *arg);

long num = 0;

int main(int argc, char *argv[])
{
    pthread_t thread_id[THREAD_NUM];
    int i;

    for (i = 0; i < THREAD_NUM; i++)
    {
        if (i % 2)
            pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
        else
            pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
    }

    for (i = 0; i < THREAD_NUM; i++)
        pthread_join(thread_id[i], NULL);

    printf("result: %ld \n", num);
    return 0;
}

void *thread_inc(void *arg)
{
    for (int i = 0; i < 100000; i++)
        num += 1;
    return NULL;
}

void *thread_des(void *arg)
{
    int i;
    for (int i = 0; i < 100000; i++)
        num -= 1;
    return NULL;
}

operation result:

        Obviously the result is not the actual desired "0" value, and the output value changes over time.

        So what causes such unrealistic values ​​to appear?

        Here is a situation:

        When thread A initiates access to variable λ=98, thread B also initiates access, so at this time both threads A and B get the value of λ=98. After the thread calculated +1 the value, it got 99 and initiated an update request to the resource variable. However, thread B also did the same operation at this time, and based on the value λ=98 that was also obtained before, Then the final result is that A calculated λ=99, B also calculated λ=99, and the last updated value was also 99, but it should actually be 100.

        In summary, the reason for this type of problem is that there is a "time difference" in accessing and processing the same resource at the same time, which leads to the deviation of the final result from the actual situation.

        After understanding the reason, this problem can be easily solved, that is, to limit the read and write permissions of the resources being accessed at the same time and synchronize the threads.

Thread synchronization

        For thread synchronization, you need to rely on the two conceptual definitions of " mutex " and " semaphore ".

mutex

        Mutex is used to limit simultaneous access by multiple threads. It mainly solves the problem of thread synchronization access and is a "lock" mechanism.

        The mutex also has special functions in the pthread.h library for creation and destruction. Let us take a look at its function structure:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t * mutex , const pthread_mutexattr_t * attr);
int pthread_mutex_destroy(pthread_mutex_t * mutex);

//成功时返回 0 ,失败时返回其他值。

/* 参数定义
 
    mutex: 创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址值。
    attr:  传递即将创建的互斥量属性,没有需要指定的属性时可以传递NULL。

        In addition, if you do not need to configure specific mutex attributes, you can initialize it by using the PTHREAD_MUTEX_INITIALIZER macro. The example is as follows:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

        However, it is best to use the pthread_mutex_init function for initialization, because it is difficult to locate the error point when debugging macros, and pthread_mutex_init's setting of mutex attributes is also more intuitive and controllable.

Mutex locking and unlocking

        The two functions mentioned above are only used for creation and destruction. The most critical ones are the two operation functions of locking and unlocking . Their structure is as follows:

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t * mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex);

// 成功时返回 0 ,失败时返回其他值 。

        The function that needs to be called before entering the critical section is pthread_mutex_lock . If it is found that other threads have entered the critical section when calling this function, the pthread_mutex_lock function will not return a value at this time unless the thread inside calls the pthreaed_mutex_unlock function to exit the critical section.

        The general structural design of the critical section is as follows:

pthread_mutex_lock(&mutex);
//临界区的开始
//..........
//..........
//临界区的结束
pthread_mutex_unlock(&mutex);

        Pay special attention to the fact that pthread_mutex_unlock() and pthread_mutex_lock() are generally in a paired relationship. If the lock is not released after the thread exits the critical section, then other threads waiting to enter the critical section will not be able to get rid of the blocking state and eventually become a "deadlock" state. .

        Next, let’s try to use a mutex to solve the previous problems.

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

#define THREAD_NUM 100

void *thread_inc(void *arg);
void *thread_des(void *arg);

long num = 0;
pthread_mutex_t mutex;

int main(int argc, char *argv[])
{
    pthread_t thread_id[THREAD_NUM];
    int i;

    pthread_mutex_init(&mutex, NULL);

    for (i = 0; i < THREAD_NUM; i++)
    {
        if (i % 2)
            pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
        else
            pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
    }

    for (i = 0; i < THREAD_NUM; i++)
        pthread_join(thread_id[i], NULL);

    printf("result: %ld \n", num);
    pthread_mutex_destroy(&mutex);
    return 0;
}

void *thread_inc(void *arg)
{
    pthread_mutex_lock(&mutex);
    // ↓临界区代码执行块
    for (int i = 0; i < 100000; i++)
        num += 1;
    // ↑临界区代码执行块
    pthread_mutex_unlock(&mutex);
    return NULL;
}
void *thread_des(void *arg)
{
    pthread_mutex_lock(&mutex);
    for (int i = 0; i < 100000; i++)
    {
        // ↓临界区代码执行块
        num -= 1;
        // ↑临界区代码执行块
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}

operation result:

        The result is finally correct~

        What needs special attention is that when designing the lock area, everyone must carefully consider the boundaries and confirm the code execution point that exactly needs to be "locked" and the "release" point that can be ended. This can avoid frequent calls to "lock". " and "unlock" operations, thereby improving the operating system's code execution efficiency.

Signal amount

        Semaphores are similar to mutexes and are also a method of thread synchronization. Generally, semaphores are represented by binary 0 and 1, so this kind of semaphore is also called "binary semaphore".

        The following are the creation and destruction methods of semaphores:

#include <semaphore.h>
int sem_init(sem_t * sem , int pshared, unsigned int value);
int sem_destroy(sem_ t * sem);

//成功时返回0,失败时返回其他值

/* 参数含义
    
    sem: 创建信号量时传递保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值。
    pshared: 传递0时,创建只允许1个进程内部使用的信号量。传递其他值时,创建可由多个进程共享的信号量。
    value: 指定新创建的信号量初始值。
*/

        Like mutexes, there are "lock" and "unlock" functions

#include <semaphore.h>
int sem_post(sem_ t * sem);
int sem_wait(sem_t * sem);

//成功时返回0,失败时返回其他值。

/* 参数含义

    sem: 传递保存信号量读取值的变量地址值,传递给sem_post时信号量增1,传递给sem_wait信号量减1。 

*/

        When calling the sem_init function, the operating system creates a semaphore object and initializes the semaphore value. The value is +1 when the sem_post function is called, and -1 when the sem_wait function is called .

        When a thread calls the sem_wait function to make the value of the semaphore 0, the thread will enter the blocking state. If other threads call the sem_post function at this time, the previously blocked thread will be able to escape the blocking state and enter the critical section.

        The critical section structure of a semaphore is generally as follows (assuming the initial value of the semaphore is 1):

sem_wait(&sem); //进入到临界区后信号量为0
// 临界区的开始
// ..........
// ..........
// 临界区的结束
sem_post(&sem); // 信号量变为1

        Semaphores are generally used to solve synchronization problems with strong order in thread tasks.

Guess you like

Origin blog.csdn.net/weixin_42839065/article/details/131532784