Linux Threads (Part 2)

       With the previous preliminary understanding of threads, we learned what threads are, the principles of threads and their control. This article will continue to explain the content and important knowledge points about threads.

Pros and cons of threads :

 Disadvantages of threads

Here we talk about thread robustness :

First of all, let's think about a question. If a thread has a problem, will it affect other threads?

Let's write code to verify:

#include <iostream>
#include <string>
#include <unistd.h>
using namespace std;


void* start_route(void* args)
{
    string name =static_cast<char*>(args);
    while(true)
    {
        cout<<"我是一个新线程,我的名字是:"<<name<<endl;
        sleep(1);

    }

}


int main()
{

    pthread_t id;
    pthread_create(&id,nullptr,start_route,(void*)"thread one");
    while(true)
    {
        cout<<"我是主线程!!!!"<<endl;
        sleep(1);

    }

    return 0;



}

The result of running the code is as follows:

 We use the ps command to view:

The same value of pid and lwp is the main thread, and the difference is the new thread created. Let's continue to modify the code to crash one of the threads and see if it affects the other process:

 The result is as follows:

When we use the ps command to search again, we find that the two threads no longer exist. The reason is: when a thread accesses the wild pointer, the operating system will send a signal to terminate the process, and the two threads have the same pid and belong to the same process. , so both threads crash at the same time!

 We said before that there is no real thread concept in the operating system. It only provides the interface of lightweight processes, and the underlying call to create processes or threads is clone :

Here we just understand the interface used by the bottom layer, we still use the usual fork and pthread_create to create.

What about threading libraries? (language version)

#include <iostream>
#include <string>
#include <unistd.h>
#include <thread>

void* thread_run()
{
    while(true)
    {
        std::cout<<"我是一个新线程!!"<<std::endl;
    }


}

int main()
{
    std::thread t1(thread_run);
    while(true)
    {
        std::cout<<"我是主线程!!!"<<std::endl;
    }
    t1.join();
}

When we use the thread in c++11, when the thread library is not introduced, the program runs and reports an error:

The error shows that there is an undefined "pthread_create". From this, it can be seen that in the linux environment, the essence of the thread in the c++ language is a further encapsulation of the thread library under linux .

The above code can also run under windows, because the language helps us solve the problem of platform differences and achieve cross-platform ! ! The interface of the native thread library is not cross-platform, and the exposure and use of the native thread library are all decided by yourself!

Global Variable Security

Now we write a ticket grabbing code, there is no problem with the ticket grabbing logic. But if multiple threads execute concurrently, a bug will appear:

#include <iostream>
#include <string>
#include <unistd.h>
#include <memory>
#include "Thread.hpp"

int ticket = 10000;
void *getTicket(void *args)
{
    string username = static_cast<const char *>(args);
    while (true)
    {
        if (ticket > 0)
        {
            usleep(1234);
            std::cout << username << "正在抢票:" << ticket << std::endl;
            --ticket;
        }
        else{
            break;
        }
    }
}

int main()
{
    std::unique_ptr<Thread> thread1(new Thread(getTicket, (void *)"user1", 1));
    std::unique_ptr<Thread> thread2(new Thread(getTicket, (void *)"user2", 2));
    std::unique_ptr<Thread> thread3(new Thread(getTicket, (void *)"user3", 3));
    std::unique_ptr<Thread> thread4(new Thread(getTicket, (void *)"user4", 4));

    thread1->start();
    thread2->start();
    thread3->start();
    thread4->start();

    thread1->join();
    thread2->join();
    thread3->join();
    thread4->join();
    return 0;
}

operation result:

At this time, we found that the number of votes turned into a negative number. The reason is that the thread was switched continuously when we performed the usleep operation. For example, when thread a just enters the judgment statement to subtract the number of votes, thread a is switched, and the context stored in the register is correspondingly switched away. At this time, thread b comes again, and it enters the judgment statement together with thread a to delete the number of votes. When b performs 20 loop operations (assuming) the number of votes is reduced by 20 times, but when thread a is switched back at this time, the cpu first reads the context in thread a, performs the subtraction operation, and finally writes the result back into memory. In this way, when thread b comes back, the result will become earth-shaking! !

The reason for the above problems is that the ++ and -- operations are not atomic , and there are at least three statements in the assembly statement:

Here I first add some concepts:

Critical resource : A shared resource that multiple execution streams access safely.

Critical Section : Code that accesses critical resources in multiple execution streams. -- tends to be a small part of the code

Mutual exclusion : Let multiple execution streams serially access a shared resource.

Atomicity : When accessing a resource, either do not do it, or do it all at once . In other words, the executed statement can be completed with one assembly .

The means to solve the above problems: lock!!!!!

Common interfaces for locks:

Initialization of the lock:

 When you define a lock, if it is a global lock it can be defined in the following way:'

 After initialization, there is no need to call the following interface on the lock:

 But if it is a local lock, the above operations of initialization and destruction of the lock are required. A global lock defined below:

The code area before lock and unlock is the critical area, and the ticket accessed in the critical area is the critical resource, and the way to access them is safe ! !

Now we use a local lock (a global lock is too simple):

class ThreadData
{
public:
    ThreadData(const string&threadname,pthread_mutex_t* pmutex =nullptr)
    :_threadname(threadname)
    ,_pmutex(pmutex)
    {

    }

    ~ThreadData(){}

public:
    string _threadname;
    pthread_mutex_t* _pmutex;

};

int ticket = 10000;
void *getTicket(void *args)
{
    ThreadData* td =static_cast<ThreadData*>(args);
    while (true)
    {
        pthread_mutex_lock(td->_pmutex);
        if (ticket > 0)
        {
            usleep(1234);
            std::cout <<td->_threadname << "正在抢票:" << ticket << std::endl;
            --ticket;
            pthread_mutex_unlock(td->_pmutex);

        }
        else{
            pthread_mutex_unlock(td->_pmutex);
            break;
        }
        
    }
    return nullptr;
}

int main()
{
    #define NUM 4
    pthread_mutex_t lock;
    pthread_mutex_init(&lock,nullptr);
    vector<pthread_t> tids(NUM);
    for(int i=0;i<NUM;++i)
    {
        char buffer[64];
        snprintf(buffer,sizeof buffer,"thread %d",i+1);
        ThreadData* td =new ThreadData(buffer,&lock);
        pthread_create(&tids[i],nullptr,getTicket,td);
    }

    for(const auto& tid:tids)
    {
        pthread_join(tid,nullptr);
    }
    
    pthread_mutex_destroy(&lock);

 

    return 0;
}

When we used the global, we found several phenomena:

1. The speed of grabbing tickets has slowed down!!! Reason: The process of locking and unlocking is executed serially by multiple threads.

2. When grabbing tickets, basically one thread grabs a lot of tickets, and other threads have no chance to grab tickets. Reason : Locks only stipulate mutually exclusive access, and locks are the result of competition among multiple execution streams . Because the logic of our ticket grabbing is not complete, after a thread releases the lock, the thread enters the loop to apply for the lock again. After grabbing tickets, you should not enter the loop again immediately, but there is a response to the result of ticket grabbing, but we didn’t write it, simply use usleep (the last line) instead:

Now let's talk about the understanding of locks :

1. When locks are used to protect shared resources, they become safe, but multiple execution streams apply for locks so that locks are also shared resources . Therefore, applying for locks and releasing locks is also atomic.

2. The use of locks to protect shared resources is actually the result of a serial execution flow. Therefore, in order to improve efficiency and speed, the granularity of the code area protected by the lock should be as small as possible .

3. If a thread applies for a lock successfully, even if the thread is switched, it is switched away holding the lock!! Therefore, when another thread applies for a lock, it must hang up and wait . The locks we have learned are also called pending wait locks.

4. Whoever holds the lock will enter the critical section !!!!!

Atomic implementation principle of lock:

Before understanding the principle, we must have two consensuses:

1. The cpu has only one set of registers shared by all execution streams.

2. The contents of the registers in the cpu are private to each execution flow and are the context of the execution flow.

Now I will show you the code implementation of the underlying principle:

 In order to ensure the atomicity of applying for locks and releasing locks, most architectures provide swap or exchange instructions. Their function is to exchange the data in the register with the data in the memory, and it is done in one step.

The way to ensure that the lock application is atomic is as follows:

First, 1 means that there is a lock, and 0 means that there is no lock. First, put 0 in the register of a thread, so that the value in the register becomes the context of thread a:

 Then use the exchange instruction to exchange the 0 in the register with the 1 in the memory, so that thread a holds the lock:

Since there is only one instruction in the assembly for exchanging values, the atomicity of the lock application is guaranteed. Then, after the lock is applied, can thread a be cut off at will? ? The answer is: of course! ! ! ! Because when thread a is cut off, its context is also cut off, so when other processes apply for locks, they cannot apply for locks in memory (a value of 0 in memory means no locks), so they are suspended wait.

If this is thread a being switched back, it will come back with its 1 (its own context). At this time, it is guaranteed that only one process holding the lock can access the critical resource! ! ! The principle of releasing the lock is almost the same as above, so I won't say much here. At this time, someone may ask: If I let a few threads have to hold a lock to access resources, and let a thread access without a lock, then can't it be guaranteed that only one thread can access it? ? ---------------It must be emphasized here that locking enables programmers to work. To access, all threads must hold locks for access. If it is special, it is your programmer A mistake in the code! ! !

Lock package design: (RAII)

First, let's introduce what RAII is:

 The following is the implementation of the code:

class Mutex
{
public:
    Mutex(pthread_mutex_t* pmutex =nullptr)
    :_pmutex(pmutex)
    {

    }
    void lock()
    {
        if(_pmutex) pthread_mutex_lock(_pmutex);

    }

    void unlock()
    {
        if(_pmutex) pthread_mutex_unlock(_pmutex);

    }

    ~Mutex()
    {

    }

    pthread_mutex_t* _pmutex;
};


class Guard_Mutex
{
public:
    Guard_Mutex(pthread_mutex_t* pmutex):_mutex(pmutex)
    {
        _mutex.lock();   //构造函数中进行加锁

    }

    ~Guard_Mutex()
    {
        _mutex.unlock(); //析构函数中进行解锁
    }

private:
Mutex _mutex;

};

Reentrant and thread safe:

The concept of reentrancy: the same function is called by different execution flows. Before an execution flow is called, other execution flows enter this function again.

The concept of reentrancy: a function will not have any problems on the premise of reentrancy, it is called a reentrant function.

Thread safety: When multiple threads concurrently execute the same piece of code, different results will not appear. This problem occurs when global variables or static variables are commonly operated and there is no lock protection.

 

 Common reentrant cases

 

I have talked so much earlier that I believe everyone has heard it in a cloud. The summary is: reentrancy is used to describe a function, and thread safety means that the result of the entire program running is correct (for example, whether there is a global variable). Data protection, whether to lock the shared resources in the function, etc.). Thread safety may or may not call a function. Therefore, reentrant functions must be thread-safe, and thread-safe functions are not necessarily reentrant.

Common Lock Concepts

The concept of deadlock:

Multiple threads continue to request each other's lock resources without releasing their own lock resources, resulting in a permanent waiting state.

Four necessary conditions for deadlock to occur :

1. Mutual exclusion: This is a feature of locks, and each resource can only be used by one execution flow at a time.

2. Request and hold conditions: Do not release the resources you have already obtained, and keep requesting the resources of the other party.

3. No deprivation: Do not forcibly obtain the resources of the other party.

4. Loop waiting: Several execution streams form a head-to-tail loop waiting resource relationship.

Ways to break deadlock:

In order to solve the deadlock problem, we need to break at least one of the necessary conditions for deadlock. Because mutual exclusion is the basic feature of locks, if there is no such thing as deadlock, we have no way to solve the mutual exclusion condition. So we solve it according to the last three:

No request and hold : If an execution flow fails to apply for a lock, it can immediately release the lock resource it owns.

Deprivation : Increase the priority of certain execution flows. Our current execution flow needs a lock but cannot apply for it, so we directly force the lock to it.

Breaking the loop waiting : If there are two locks A and B, the two execution flows apply and release the locks in the order of A and B in turn, instead of one applying for A first and then applying for B, and the other applying for B first and then applying for A.

The second part of the thread is over here, and the second part of the thread will continue to be updated. I hope everyone will support it!

Guess you like

Origin blog.csdn.net/m0_69005269/article/details/132407450