[Linux] Thread synchronization and deadlock

Table of contents

deadlock

what is deadlock

Four necessary conditions for a deadlock

How to avoid deadlock

thread synchronization

introduction of synchronization

way of synchronization

condition variable

Use of condition variables

overall code


 

deadlock

what is 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 is in a permanent waiting state due to mutual application for resources that are occupied by other processes and will not be released  .

        For example, the process has two locks, mtx1 and mtx2. When process A executes a certain code, it needs to apply for mtx1 first, and then apply for mtx2; while process B executes the corresponding code, it needs to apply for mtx2 now, and then apply for mtx1.

        At a certain moment, processes A and B run at the same time, process A gets mtx1, process B gets mtx2, but then, process A needs mtx2, but at this time the lock is occupied by process B and cannot apply, so it blocks and waits Completion of Process B. Process B needs mtx1, but it is occupied by A at this time, and needs to wait for the completion of process A, which is also blocked here. So it caused a deadlock.

        

Four necessary conditions for a deadlock

  • Mutually exclusive conditions:  a resource can only be used by one execution flow at a time, and other processes cannot access the resource at the same time.
  • Request and hold conditions: that is, the process can keep the resources it has occupied when requesting resources , that is, it does not release its original resources
  • No deprivation condition: A process that has acquired resources cannot be forcibly deprived of resources owned by other processes , only voluntary release
  • Circular waiting condition: A head-to-tail circular waiting resource relationship is formed among several processes . Such as A->B, B->A.

How to avoid deadlock

     We know the four necessary conditions that constitute a deadlock, as long as any one of the conditions is destroyed :

  • Destruction of mutual exclusion conditions: Try to avoid using mutually exclusive resources, or use different resource access methods, such as read-write locks, allowing multiple processes or threads to access certain resources at the same time.
  • Destroy the request and hold conditions: If the application for multiple locks fails, the resources you already own are released .
  • Break the non-deprivation condition: introduce a resource preemption mechanism, which allows the operating system to preempt the resources that the process has acquired. When other processes urgently need a certain resource, the system can terminate or suspend a certain process, and release the resources held by it to the required process.
  • Break the loop waiting condition: adopt the global resource sorting strategy, specify a unique number for each resource, and then require the process to request resources only in the order of increasing number, so as to avoid the occurrence of loop waiting.

   The "resources" mentioned above can also be understood as locks.

thread synchronization

introduction of synchronization

        What we said in the last chapter        

        1. An example of multi-threading and then grabbing tickets. We found that although there are multiple threads, it is basically the thread that is grabbing each time (for example, the priority may be higher), and other threads cannot grab it. This is a thread that frequently To apply for resources, causing other thread starvation problems.

        2. Assume that a resource is temporarily gone, and the thread is still competing for the lock , and then accesses the resource. If the resource cannot be accessed, the lock is not released, and it continues like this, but there is no resource available at this time. This would be too wasteful.

        All of the above operations are correct, but unreasonable !

        So in order to solve the above series of problems, synchronization is introduced: mainly to solve the rationality of accessing critical resources .

That is, access to critical resources is performed in a certain order .

        1. For problem 1, we can do this: when a thread applies for a resource, after using it, it is queued behind other threads, allowing other threads to access it first, and so on.

        2. For question 2, we can temporarily issue a number for each thread, and when resources are available, access the resources in the order of the numbers instead of unfairly competing with each other for this resource

So thread synchronization is: Thread synchronization refers to the process of coordinating the execution order of multiple threads and access to shared resources in concurrent programming to ensure the correct interaction between threads

way of synchronization

condition variable

        When we apply for critical resources ---> check whether the critical resources exist --- > the essence of detection: access to critical resources ---> conclusion: the detection of critical resources must also be locked and unlocked between.

        In this case, the detection still requires frequent application and release of locks, so is there a way for the thread to detect when the resource is not ready:

        a. Don't let the thread detect itself frequently, but wait

        b. When the conditions are ready, notify the corresponding thread to allow it to apply for and access resources.

In order to meet the above statement, here is the introduction of conditional variables.

        Condition variable (Condition Variable) is a synchronization primitive, which is often used for waiting and notification between threads in multi-threaded programming. It is used to achieve synchronization and cooperation between threads , so that a thread can wait for a certain condition to be met and be notified by other threads to wake up.

Use of condition variables

        The use of condition variables requires a mutex (pthread_mutex_t) to ensure thread-safe operation. The general usage steps are as follows:

        First of all, we need to create a condition variable , the data type is  pthread_cond_t, and we also need to create a mutex.

        

    pthread_mutex_t mtx;
    pthread_cond_t cond;

       Initialize condition variables and mutexes: We have talked about the initialization method for mutexes, and here is the function to initialize condition variables:

 pthread_cond_init, the function prototype is as follows:

       int pthread_cond_init(pthread_cond_t *restrict cond,
              const pthread_condattr_t *restrict attr);

        The first parameter is the condition variable, and the second parameter is the property of the condition variable, which is generally set to NULL.

        If the created condition variable is global , it can be initialized in the following way:

       pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

So the initialization code is as follows: 

    pthread_cond_init(&cond,nullptr);//初始化条件变量
    pthread_mutex_init(&mtx,nullptr);//初始化锁

Next, we create 4 threads, and then create a structure, which contains the thread name, the method called by the thread, condition variables and mutexes, and then write a constructor to initialize these:

typedef void(*func_t)(const string& name,pthread_mutex_t* pmtx, pthread_cond_t* pcond);
class ThreadData
{
public:
    ThreadData(const string& name,func_t func,pthread_mutex_t* pmtx, pthread_cond_t* pcond)
    :_name(name),_func(func),_pmtx(pmtx),_pcond(pcond)
    {}
public:
    string _name;//线程名
    func_t _func;//该线程对应的回调方法
    pthread_mutex_t* _pmtx;//互斥锁
    pthread_cond_t* _pcond;//条件变量
};

        Then start to write the callback method of each thread. In fact, the function of the lock in the condition variable cannot be well displayed here. We need to see the effect in the next chapter of the producer and consumer model.

        For each thread method here, we first use pthread_cond_wait to block and wait, and when the resources are ready, we will continue to execute backwards. Then output a statement later,

        The function prototype is as follows:

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

        The function pthread_cond_wait is used to make the thread enter the waiting state, and will atomically release the mutex specified by mutex. After entering the waiting state, it will block the wait until other threads call pthread_cond_signal or pthread_cond_broadcast with the same condition variable , wake up and restart Acquire a mutex. You need to ensure that the lock has been locked before calling . The mutex is reacquired on return.

        The first parameter is the condition variable, and the second parameter is the mutex . The specific function will be discussed in the next class.

        This is the callback method of thread 1. Threads 2, 3, and 4 are the same, but with different names.

void func1(const string& name,pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{
    while(true)
    {
        pthread_mutex_lock(pmtx);
        //if(临界资源不就绪) wait之前一般会进行检测,这里由于无法很好的模拟场景,就暂时不加if
        pthread_cond_wait(pcond,pmtx);//默认该线程在执行的时候,wait代码被执行,当前线程会立即被阻塞
        cout << name << "running ..." << endl;
        pthread_mutex_unlock(pmtx);
    }
}

        Now after the threads are created, each thread is blocked in the pthread_mutex_wait interface, so we need to wake up these threads in the main function . There are two ways:

pthread_cond_signal

int pthread_cond_signal(pthread_cond_t *cond);

The parameter is a condition variable . As for which thread to wake up, it is determined by the scheduler, but the order must be fixed. When we run the program:

        This ensures thread synchronization

This is a thread-by-thread wakeup. If we want to wake up all threads, we need to use pthread_cond_broadcast,

The function prototype is as follows:

int pthread_cond_broadcast(pthread_cond_t *cond);

The parameter is also a condition variable, and the function pthread_cond_broadcast is used to broadcast the signal of the condition variable, that is, to wake up all threads waiting for the condition variable.

 In this way, all threads can be woken up at once to continue execution.


After everything is done, we need to destroy and release the condition variable and mutex at the end. The function is pthread_cond_destroy,

The function prototype is:

       int pthread_cond_destroy(pthread_cond_t *cond);

The parameter is also a defined condition variable, after passing it in, the condition variable can be released.


overall code

        The above is a general process of using condition variables. The specific understanding of the producer-consumer model will be explained in the next chapter. This column only needs to understand the usage of condition variables.

        You can copy it to your own platform to run, remember to add -lpthread when compiling, as follows:

g++ -o mythread mythread.cc -lpthread

code:

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

using namespace std;

#define PTHREAD_NUM 4

typedef void(*func_t)(const string& name,pthread_mutex_t* pmtx, pthread_cond_t* pcond);
class ThreadData
{
public:
    ThreadData(const string& name,func_t func,pthread_mutex_t* pmtx, pthread_cond_t* pcond)
    :_name(name),_func(func),_pmtx(pmtx),_pcond(pcond)
    {}
public:
    string _name;
    func_t _func;
    pthread_mutex_t* _pmtx;
    pthread_cond_t* _pcond;
};

void func1(const string& name,pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{
    while(true)
    {
        pthread_mutex_lock(pmtx);
        //if(临界资源不就绪) wait之前一般会进行检测,这里由于无法很好的模拟场景,就暂时不加if
        pthread_cond_wait(pcond,pmtx);//默认该线程在执行的时候,wait代码被执行,当前线程会立即被阻塞
        cout << name << "running ..." << endl;
        pthread_mutex_unlock(pmtx);
    }
}
void func2(const string& name,pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{
    while(true)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcond,pmtx);//默认该线程在执行的时候,wait代码被执行,当前线程会立即被阻塞
        cout << name << "running ..." << endl;
        pthread_mutex_unlock(pmtx);
    }
}
void func3(const string& name,pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{
    while(true)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcond,pmtx);//默认该线程在执行的时候,wait代码被执行,当前线程会立即被阻塞
        cout << name << "running ..." << endl;
        pthread_mutex_unlock(pmtx);
    }
}
void func4(const string& name,pthread_mutex_t* pmtx, pthread_cond_t* pcond)
{
    while(true)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcond,pmtx);//默认该线程在执行的时候,wait代码被执行,当前线程会立即被阻塞
        cout << name << "running ..." << endl;
        pthread_mutex_unlock(pmtx);
    }
}


void* Entry(void* args)
{
    ThreadData* td = (ThreadData*)args;//td在每一个线程自己私有的栈空间保存
    td->_func(td->_name,td->_pmtx,td->_pcond);
    delete td;

    return nullptr;
}
int main()
{
    pthread_mutex_t mtx;
    pthread_cond_t cond;

    pthread_mutex_init(&mtx,nullptr);
    pthread_cond_init(&cond,nullptr);


    pthread_t tids[PTHREAD_NUM];
    //定义四个线程的回调方法
    func_t funcs[PTHREAD_NUM] = {func1,func2,func3,func4};
    for(int i = 0; i < PTHREAD_NUM; i++)
    {
        string name = "thread ";
        name += to_string(i+1);
        ThreadData* td = new ThreadData(name,funcs[i],&mtx,&cond);
        pthread_create(tids+i,nullptr,Entry,(void*)td);
    }

    //这里为了方便演示pthread_cond_wait,在没有用signal或broadcast唤醒前,一直处于阻塞状态
    sleep(5);
    //控制线程
    while(true)
    {
        cout << "wake up thread run code ..." << endl;
        //pthread_cond_signal(&cond);//唤醒一个线程
        pthread_cond_broadcast(&cond);//唤醒全部线程
        sleep(1);

    }

    for(int i = 0; i < PTHREAD_NUM; i++)
    {
        pthread_join(tids[i],nullptr);
    }

    pthread_mutex_destroy(&mtx);
    pthread_cond_destroy(&cond);
    return 0;
}

Guess you like

Origin blog.csdn.net/weixin_47257473/article/details/132257979