[Linux] Interview focus: deadlock and production consumption model principle

The interview points are coming~

Article Directory


foreword

In the principle of mutex in the previous article, we explained the principle of locks. We know that every time a thread applies for a lock, once the application is successful, the thread itself takes the lock with itself, which ensures the atomicity of the lock (because There is only one lock), and what happens when we have successfully applied for the lock and then apply for the lock? Below we answer this question in deadlock.


1. Deadlock

Deadlock refers to a permanent waiting state in which each process in a group of processes occupies resources that will not be released, but they apply to each other for resources that are occupied by other processes and will not be released.
The above concept of deadlock refers to each process in a group of processes, so will a lock cause a deadlock problem? The answer is yes, because the code is written by programmers, so if there is a problem with the code, even a lock will cause a deadlock problem, for example: apply for a lock but not release it. After successfully applying for a lock, apply for the same lock. In this case, the application will definitely fail. Once the application fails, the thread will be blocked and suspended, and the lock will become a deadlock. After applying for a lock, I applied for other locks. As a result, other lock applications failed and were blocked. Once blocked, the process will sleep with the lock that was successfully applied for the first time, which will cause a deadlock problem (because A lock will never be successfully applied by a thread).
Of course, there are many conditions that cause deadlock. Let's give the conclusion directly below:
1. Mutual exclusion
2. Request and hold (I take mine and apply for yours)
3. Loop wait (loop wait condition)
4. No deprivation of conditions
To avoid deadlock, we can directly destroy any of the above 4 conditions. It is very simple to destroy the mutual exclusion condition, we just don't lock it. For the condition of requesting and maintaining, as long as we actively release the lock, there will be no such problem. Loop waiting means that the order in which multiple threads apply for locks is different, resulting in a "request and hold" situation for some threads. For this problem, we only need to let each thread apply for locks in order. The fourth condition means: a thread applies for a lock and can only release the lock by itself. In this case, once the thread does not release the lock, it will cause a deadlock problem, so we need to deprive the thread of the condition for releasing the lock. Let other threads release the thread that did not release the lock just now. Some people may be confused here, can one thread apply for a lock, and another thread release the lock just applied by that thread? The answer is yes, let's demonstrate it with code:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *threadRoutine(void* args)
{
    cout<<"我是一个新线程"<<endl;
    pthread_mutex_lock(&mutex);
    cout<<"我拿到了锁"<<endl;
    pthread_mutex_lock(&mutex);//由于再次申请锁的问题会停下来(死锁)
    cout<<"我又活了过来"<<endl;
    return nullptr;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,threadRoutine,nullptr);
    sleep(3);
    cout<<"主线程正在运行"<<endl;
    return 0;
}

First of all, let's not let the main thread release the lock, and look at the state of the deadlock:

 We can see that "I am alive again" should have been printed, but it exited directly. Let's let the main thread unlock the new thread:

Through the running results, we can see that the deadlocked process has come alive, which means that one thread can apply for a lock and another thread can release the lock. Therefore, it is necessary to destroy the fourth condition to directly control a thread to release the lock uniformly.

Here we summarize:

Ways to avoid deadlock:
1. Four necessary conditions for breaking deadlock
2. The locking sequence is consistent
3. Avoid scenarios where locks are not released
4. One-time allocation of resources
The following explains the condition variables in linux thread synchronization:
When a thread has exclusive access to a variable, it may find that it can do nothing until other threads change the state.
For example, when a thread accesses the queue and finds that the queue is empty, it can only wait until other threads add a node to the queue. In this case, condition variables are used.
Synchronization concepts and race conditions:
Synchronization: On the premise of ensuring data security, allowing threads to access critical resources in a specific order, thereby effectively avoiding starvation problems, is called synchronization.
Race condition: Because of timing problems, the program is abnormal, we call it a race condition. In the threading scenario, this kind of problem is not difficult to understand.
Let's talk about the condition variable function initialization interface:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrictattr);
parameter:
cond : the condition variable to initialize
attr NULL
Destroy the condition variable:
int pthread_cond_destroy(pthread_cond_t *cond)
Waiting on a condition variable:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
parameter:
cond : to wait on this condition variable
mutex : mutex
Note: The meaning of condition variables with locks is: when we put a thread in a condition variable (possibly a sequence container such as a queue) to wait, the condition variable will automatically lock the thread we are waiting for in order to prevent deadlock problems. Released, that is to say, otherwise a thread holds the lock while waiting.
Of course, there is also an interface for waking up condition variables:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
Below we use code to demonstrate how to use these interfaces:
const int num = 5;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *active(void* args)
{
    string name = static_cast<const char*>(args);
    while (true)
    {
        pthread_mutex_lock(&mutex); //先加锁
        //然后放进条件变量去等
        pthread_cond_wait(&cond,&mutex); //调用的时候会自动释放锁
        cout<<name<<" 活动 "<<endl;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
    pthread_t tids[num];
    for (int i = 0;i<num;i++)
    {
        char* name = new char[32];
        snprintf(name,32,"thread -> %d ",i+1);
        pthread_create(tids+i,nullptr,active,name);
    }
    sleep(3);
    while (true)
    {
        cout<<"主线程唤醒其他线程......"<<endl;
        //在环境变量中按顺序唤醒一个线程
        pthread_cond_signal(&cond);
        sleep(1);
    }
    for (int i = 0;i<num;i++)
    {
        pthread_join(tids[i],nullptr);
    }
    return 0;
}

The function of the above code is: first create 5 threads, these 5 threads must enter the active function, first add a lock in the function, and then let this thread wait in the environment variable, and release the lock by itself during the waiting process, Then after 3 seconds, the main thread starts to wake up the threads in these environment variables. The function of the pthread_cond_signal interface is to wake up one thread at a time. Each wakeup is woken up in order. After waking up, the thread will print "activity", as follows Let's run it and see:

 You can see that the sequence of wakeups here is 31245. Next, we can use the pthread_cond_broadcast interface to wake up all interfaces at once:

 Through the running results, we can see that the wake-up is also performed in order, so the function of the condition variable is to allow multiple threads to wait in queue in cond (queue waiting is a kind of order).

Here we summarize:

Why does pthread_cond_wait need a mutex ?
Conditional waiting is a means of synchronization between threads. If there is only one thread and the condition is not satisfied, it will not be satisfied if it continues to wait. Therefore, there must be a thread that changes the shared variable through certain operations, so that the original unsatisfied condition becomes must be satisfied, and friendly notify threads waiting on the condition variable.
Conditions will not suddenly become satisfied for no reason, and it will inevitably involve changes in shared data. So be sure to use a mutex to protect. Shared data cannot be safely accessed and modified without a mutex.
Since unlocking and waiting are not atomic operations. After calling unlock, before pthread_cond_wait , if other threads have acquired the mutex, the abandonment condition is met, and the signal is sent, then pthread_cond_wait will miss this signal, which may cause the thread to be blocked in this pthread_cond_wait forever . So unlocking and waiting must be an atomic operation.
int pthread_cond_wait(pthread_cond_ t *cond, pthread_mutex_ t * mutex) ; After entering this function, will you see if the condition value is equal to 0 ? If it is equal, change the mutex to 1 until cond_wait returns, change the condition to 1 , and restore the mutex
As is.

 2. Producer-consumer model

Let's first draw a picture to understand the producer-consumer model:

 First of all, in the model, the supermarket does not belong to the producer. The supermarket is just a trading place. The supplier will put the produced goods in the supermarket, and then the consumer will go directly to the supermarket to buy. Why don't consumers go directly to the supplier to buy? Because consumers don't know when the suppliers will produce the goods, and the demand of consumers is very fragmented, and the production level of suppliers is very strong, resulting in a natural barrier between the two. So what are the characteristics of this model?

The first point is high efficiency, and the second point is uneven busyness. What does the second point mean? It means that we can put a large number of commodities produced by suppliers in the supermarket for consumers to buy anytime and anywhere. When suppliers are busy producing commodities, when the commodities produced have not been purchased or there are still many When the goods are stored, the supplier will not produce too much, and this time is idle. Now let's make the model concrete, the consumer is actually a thread, the producer (supplier) is also a thread, and the supermarket, the trading place, is actually a specific buffer (queue, chain, hash, etc.), The commodities in the supermarket above are actually data. What do you think of this model? That’s right, it’s pipeline communication. Isn’t a pipeline just that one process puts data in the buffer and another process takes it from the buffer? We wrote an example of code that closed the read end and closed the write end. I don’t know if you remember it.

For the buffer we just mentioned, this buffer must be seen by both consumers and producers, so it is destined that the trading place must be a public area that will be accessed concurrently by multiple threads, and it is destined to be multi-threaded. Threads must protect the security of shared resources, and it is doomed to maintain the relationship between thread mutual exclusion and synchronization in this case.

Let's explain the relationship in the model:

1. Producer and producer: There must be a mutually exclusive relationship between producers and producers, because they are all rushing to store resources in the buffer, only whoever produces faster will put them into the buffer first.

2. Consumers and consumers: The relationship between consumers and consumers must also be mutually exclusive , because when there is only one product, then two consumers will definitely grab this product, which reflects mutuality at this time. Rejection.

3. Producers and consumers: There is a synchronous relationship between producers and consumers . Because when the product is produced, the producer will continue to produce only after the consumer consumes it, otherwise the buffer is full, even if the producer produces again, it will not be put into the buffer. There is a mutually exclusive relationship between the second producer and the consumer , because we cannot let the consumer send things to the buffer while taking things in the buffer, there can only be one between the producer and the consumer into the buffer.

Let's record the producer-consumer model in a simple way:

3 relationships (producer and producer, consumer and consumer, producer and consumer), two roles (producer and consumer), one transaction place (usually a buffer). So we only need to remember 321 for the producer-consumer model in the future.


Summarize

Why use the producer consumer model
The producer-consumer model uses a container to solve the problem of strong coupling between producers and consumers. Producers and consumers do not communicate directly with each other, but communicate through blocking queues. Therefore, after producing data, producers do not need to wait for consumers to process it, but directly throw it to the blocking queue. Consumers do not ask producers for data, but Take it directly from the blocking queue, which is equivalent to a buffer, which balances the processing capabilities of producers and consumers. This blocking queue is used to decouple producers and consumers.

Guess you like

Origin blog.csdn.net/Sxy_wspsby/article/details/131018408