Mutex locks, condition variables, semaphores and applicable scenarios

Article directory

    • mutex lock
      • Commonly used methods in the actual combat of mutex locks
    • condition variable
      • Commonly used methods in the actual combat process of condition variables
    • signal
      • Common methods of semaphores
    • producer and consumer issues
      • A rough version of the producer-consumer code (if only mutexes were used)
      • An improved version of the producer-consumer code (using mutex locks and condition variables)
      • A final version of the producer-consumer code (using mutexes and semaphores)
    • Summarize

Mutex lock

Mutex locks are mainly used in multi-thread programming. When multiple threads access the same variable at the same time, it is guaranteed that only one thread can access it at a certain time. When each thread accesses a shared variable, it must first acquire the lock before it can access the shared variable. When a thread successfully acquires the lock, other variables will be blocked in the step of acquiring the lock until the thread releases the lock.

Commonly used methods in the actual implementation of mutex locks

pthread_mutex_t mutex;//Create a mutex
pthread_mutex_init( & amp;mutex,NULL); //Initialize lock
pthread_mutex_lock( & amp;mutex); //Lock the segment that needs to be locked
pthread_mutex_unlock( & amp;mutex); // Unlock the segment that needs to be locked
pthread_mutex_destroy( & amp;mutex);//Destroy the lock after the lock is used up

Conditional variable

The purpose of condition variables is to thread synchronization between multiple threads. Thread synchronization refers to the behavior that needs to be carried out between threads in a predetermined order. For example, I want to allow thread 2 to start working only after thread 1 has completed a certain step. At this time, I can use condition variables to achieve the purpose.

Commonly used methods in practice with condition variables

pthread_cond_t cond;//Create condition variable
pthread_cond_init( & amp;cond,NULL);//Initialize condition variables
pthread_cond_signal( & amp;cond);//Usually used with the wait function to send a wake-up signal to the condition variable
pthread_cond_wait( & amp;cond, & amp;mutex);//Block first until awakened after receiving signal or broadcast
pthread_cond_destroy( & amp;cond);//Destroy the condition variable after use

Semaphore

Semaphores can be used to solverace conditions in multi-threaded environments and coordinate the execution order of threads

Common methods of semaphore

int sem_init(sem_t *sem,int pshared,unsigned int value);//Function: Initialize the semaphore Parameter pshared: 0 is used between threads, non-0 is used between processes
int sem_wait(sem_t *sem);//Decrease the value of the semaphore by 1, and block if the value is 0
int sem_post(sem_t *sem);//Unlock the semaphore, call it once to add 1 to the value of the semaphore
int sem_destroy(sem_t *sem);//Release resources

Producer and consumer issues

Producers: The task of the producer process or thread is to generate some type of data or items and put them into the shared buffer. The producer may generate multiple data items and needs to ensure that the buffer is not full when placing data into it.
Consumers: The task of consumer processes or threads is to retrieve data items from the shared buffer and process or consume them. The consumer may be getting data at a certain rate and needs to make sure the buffer is not empty.
Shared buffer (Buffer): This is a shared data structure between producers and consumers, used to store data items generated by producers. The capacity of the buffer is limited, the producer needs to wait when the buffer is full, and the consumer needs to wait when the buffer is empty.

A rough version of the producer-consumer code (if only mutex locks are used)

//Producer part code
void * producer(void * arg){<!-- -->
    //Producers continue to produce
    //Continuously create new nodes
    while(1){<!-- -->
        pthread_mutex_lock( & amp;mutex);
        struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode ->next =head;
        head = newNode;
        newNode ->num = rand() 00;
        printf("add node,num:%d,tid:%ld\
",newNode->num,pthread_self());
        pthread_mutex_unlock( & amp;mutex);
        usleep(100);
    }
    
    return NULL;
}
void * customer(void * arg){<!-- -->
    //Consumers continue to consume data
    while(1){<!-- -->
        pthread_mutex_lock( & amp;mutex);
        if(head){<!-- -->
            //If there is data
            struct Node * tmp =head;
            head = head->next;
            printf("del node,num:%d,tid:%ld\
",tmp->num,pthread_self());
            free(tmp);
            pthread_mutex_unlock( & amp;mutex);
            usleep(100);
        }else{<!-- -->
            pthread_mutex_unlock( & amp;mutex);
           
        }
    }
    return NULL;
}

Although a mutex lock is used to achieve mutually exclusive access by several threads of the producer and several threads of the consumer, if the consumer thread grabs the lock and has no resources to consume, it will idle and consume the CPU, preempting It’s useless to reach the lock

An improved version of the producer-consumer code (using mutex locks and condition variables)

void * producer(void * arg){<!-- -->
    //Producers continue to produce
    //Continuously create new nodes
    while(1){<!-- -->
        pthread_mutex_lock( & amp;mutex);
        struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode ->next =head;
        head = newNode;
        newNode ->num = rand() 00;
        printf("add node,num:%d,tid:%ld\
",newNode->num,pthread_self());
        pthread_cond_signal( & amp;cond);
        pthread_mutex_unlock( & amp;mutex);
        usleep(100);
    }
    
    return NULL;
}
void * customer(void * arg){<!-- -->
    //Consumers continue to consume data
    while(1){<!-- -->
        pthread_mutex_lock( & amp;mutex);
        if(head){<!-- -->
            //If there is data
            struct Node * tmp =head;
            head = head->next;
            printf("del node,num:%d,tid:%ld\
",tmp->num,pthread_self());
            free(tmp);
            pthread_mutex_unlock( & amp;mutex);
            usleep(100);
        }else{<!-- -->
            //If there is no data, it needs to wait and is blocked. It will not wake up until the producer executes pthread_cond_signal(&cond)
            pthread_cond_wait( & amp;cond, & amp;mutex);
            //This function plays two roles,
            //1. Release the mutex first to allow other code to be executed.
            //2. It is not until pthread_cond_signal or pthread_cond_broadcast is executed that the function is awakened and the lock is re-locked.
            //Release the lock after the blocking is released
            pthread_mutex_unlock( & amp;mutex);
        }
    }
    return NULL;
}

The above method has solved the problem of CPU idling when the producer has not yet produced resources. In the code, condition variables are used. When resources are not produced, the consumer will block in pthread_cond_wait( & amp;cond, & amp;mutex); it will wake up only when the producer calls pthread_cond_signal( & amp;cond) The consumer continues to execute and will not seize the lock from the producer!
Butthe situation of limited capacity is not considered, which means that the producer can keep producing!

A final version of the producer-consumer code (using mutex locks and semaphores)

void * producer(void * arg){<!-- -->
    //Producers continue to produce
    //Continuously create new nodes
    while(1){<!-- -->
        sem_wait( & amp;psem); // Call once to determine whether it is 0. If it is 0, it will block. If it is not 0, the value will be reduced by 1.
        //Cannot be placed in the lock. If it is placed in the lock, after grabbing the lock and blocking wait, it will be deadlocked forever.
        //wait can unlock the lock through a post from another thread
        //After adding the mutex lock, it can only be released through its own code
        pthread_mutex_lock( & amp;mutex);
        struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode ->next =head;
        head = newNode;
        newNode ->num = rand() 00;
        printf("add node,num:%d,tid:%ld\
",newNode->num,pthread_self());

        pthread_mutex_unlock( & amp;mutex);
        sem_post( & amp;csem);//Indicates that the consumer has one to consume
        usleep(100);
    }
    return NULL;
}
void * customer(void * arg){<!-- -->
    //Consumers continue to consume data
    while(1){<!-- -->
            sem_wait( & amp;csem); // First determine whether it is 0. Block if it is 0. If it is not 0, consume one csem.
            pthread_mutex_lock( & amp;mutex);
            struct Node * tmp =head;
            head = head->next;
            printf("del node,num:%d,tid:%ld\
",tmp->num,pthread_self());
            free(tmp);
            pthread_mutex_unlock( & amp;mutex);
            sem_post( & amp;psem); //Add a production location
            usleep(100);
    }
    return NULL;
}

The final version achieves inter-thread synchronization through semaphores, and can also achieve limited buffer capacity without deadlock. Thesemaphore must be placed before the mutex lock! ! !

Summary

The mutex lock realizes mutually exclusive access to a certain segment between threads, and realizes mutually exclusive access of threads. Only one can hold the lock at the same time!
Condition variables are usually used with mutex locks to achieve access between threads in a certain order, that is, thread synchronization.
Semaphores dynamically adjust throughchanges in resources held by consumers and producers. When a product is produced, it means there is one more consumable product. When a product is consumed, it means that another product can be produced.