5. Multi-threaded concurrent lock

This article introduces the errors that occur in order to avoid the preemption of critical resources under multi-threaded concurrency, and introduces locks and atomic operations to solve them.

1. Problem analysis

Create 10 threads, and each thread adds 10,000 numbers to the total process. Then the total process will reach 100,000

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

#define THREAD_COUNT    10

void *thread_callback(void* arg){
    
    
    int *pcount=(int *)arg;
    int i=0;
    while(i++ <10000){
    
    
        (*pcount)++;

        //usleep(1)是一个系统调用函数,它的作用是让当前进程暂停执行一微秒(即等待一微秒
        usleep(1);
    }
}

int main(){
    
    
    pthread_t threadid[THREAD_COUNT]={
    
    0};
    int i=0;
    int count=0;

    //创建10个线程
    for (i=0;i<THREAD_COUNT;i++){
    
    
        pthread_create(&threadid[i],NULL,thread_callback,&count);
    }

    for (i=0;i<100;i++){
    
    
        printf("count :%d\n",count);

        //sleep(n)是一个系统调用函数,它的作用是让当前进程暂停执行n秒钟(即等待n秒钟)
        sleep(1);
    }
}

But in the process of implementation, you will find that it will not reach 1 million in the end. The reason for this phenomenon is that count is a critical resource, that is, multiple threads share a variable. Let's analyze the assembly instructions of count++

mov count,eax;
inc eax;
mov eax,count;

In practice, the following exceptions may occur.
insert image description here
The solution is to lock, that is, to package and lock the three assembly instructions of count++, which cannot be separated and cannot be preempted by other threads.

2. Locks and atomic operations

insert image description here
Turning the three assembly instructions of count into one instruction does not require locking. This is called an atomic operation.

Atomic operation refers to an operation mode that can guarantee the indivisibility and exclusiveness of the operation when accessing or modifying shared resources in a concurrent environment. Usually, atomic operations are supported by the hardware level, which can ensure that they will not be interrupted or interfered by other threads during execution, thereby avoiding problems such as race conditions.

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

#define THREAD_COUNT    10
pthread_mutex_t mutex;
pthread_spinlock_t spinlock;

//对value加上add
int inc(int *value,int add){
    
    
    int old;

    //__asm__是GCC的内嵌汇编语言指令
    __asm__ volatile(
        "lock;xaddl %2,%1;"// %2的值+%1的值,赋给%1;同时,在执行该指令期间,CPU会对总线进行锁定(lock),以确保整个操作是原子性地完成。
        : "=a" (old)
        : "m" (*value),"a"(add)
        : "cc","memory"
    );
    return old;
}

void *thread_callback(void* arg){
    
    
    int *pcount=(int *)arg;
    int i=0;
    while(i++ <10000){
    
    
#if 0
    (*pcount)++;
#elif 0
    pthread_mutex_lock(&mutex);
    (*pcount)++;
    pthread_mutex_unlock(&mutex);
#elif 0
    pthread_spin_lock(&spinlock);
    (*pcount)++;
    pthread_spin_unlock(&spinlock);
#else
    inc(pcount,1);
#endif

    usleep(1);
    }
}

int main(){
    
    
    pthread_t threadid[THREAD_COUNT]={
    
    0};

    pthread_mutex_init(&mutex,NULL);//初始化mutex
    pthread_spin_init(&spinlock,PTHREAD_PROCESS_SHARED);//初始化spinlock,进程之间共享

    int i=0;
    int count=0;

    //创建10个线程
    for (i=0;i<THREAD_COUNT;i++){
    
    
        pthread_create(&threadid[i],NULL,thread_callback,&count);
    }

    for (i=0;i<100;i++){
    
    
        printf("count :%d\n",count);

        //sleep(n)是一个系统调用函数,它的作用是让当前进程暂停执行n秒钟(即等待n秒钟)
        sleep(1);
    }
}

3. Supplementary knowledge points

1. Create a process

pthread_t is a thread identifier, which can uniquely identify a thread. In multi-threaded programming, we usually use the pthread_create() function to create a new thread, and this function returns a value of type pthread_t so that we can operate on this new thread in subsequent code.

pthread_create() is a function used to create a new thread. Its prototype is as follows:

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

The function accepts four parameters:

  • thread: A pointer to a variable of type pthread_t, used to store the identifier of the new thread.
  • attr: A pointer to a variable of type pthread_attr_t, which is used to set the attributes of the new thread (such as stack size, priority, etc.). If NULL, default properties are used.
  • start_routine: pointer to the function to be executed by the new thread. The function must return a void* type, and can only have one parameter, which corresponds to the parameter passed to it when the function is called.
  • arg: Arguments passed to the start_routine function.

For example, here is a simple example that demonstrates how to use pthread_create() to create a new thread:

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

void* thread_func(void *arg) {
    
    
    printf("New thread created\n");
}

int main() {
    
    
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, thread_func, NULL);
    if (ret != 0) {
    
    
        printf("Error creating thread\n");
        return -1;
    }
    
    // Wait for the new thread to complete
    pthread_join(tid, NULL);
    
    return 0;
}

In the above code, we defined a function called "thread_func" as what the new thread will execute. In the main function, we call the pthread_create() function to create a new thread, and store the return value of this function in the variable tid. Then, we wait for the new thread to finish executing by calling the pthread_join() function. Inside the new thread, we simply print a message indicating that it was successfully created.

2. Conditional compilation instructions

#if 0
    (*pcount)++;
#else 
    pthread_mutex_lock(&mutex);
    (*pcount)++;
    pthread_mutex_unlock(&mutex);
#endif

This is a preprocessor directive used to control the execution of code at compile time. If the #if 0 section is commented out code or code that does not need to be executed, you can use #if 0 to temporarily disable them so as not to waste system resources and time. Similarly, #elif 0 can be used to replace a part of code that is commented out or does not need to be executed, while #else is a statement block to be executed when all the above conditions are not met.

The advantage of using conditional compilation directives is that you can control the execution of code at compile time according to the macros defined by the preprocessor, without the need to manually comment or uncomment a large block of code. This approach is very useful when debugging and testing code, because the implementation of certain features can be easily turned on or off, which simplifies code maintenance and debugging.

Guess you like

Origin blog.csdn.net/Ricardo2/article/details/130791416