【LINUX】Multithreading

Threads and Processes

Definition: thread is an execution score, the execution granularity is finer than that of process, and the scheduling cost is lower. Thread is an execution flow inside the process. Kernel: thread is the basic
unit of CPU scheduling, and process is the basic entity responsible for allocating system resources.

A thread is a control sequence inside a process. A process must have at least one execution thread. The thread runs inside the process and essentially runs in the process address space. From the perspective of the CPU, the PCB of the thread is lighter than the process. Through the virtual address of the process Space, you can see most of the resources of the process, and allocate them reasonably to each execution flow to form a thread execution flow.

Advantages of threads:

1. In the Linux operating system, the task_struct of the thread is also managed like the pcb of the process. Compared with the pcb of the process, the pcb of the thread is lighter. Therefore, the cost of creating a new thread is smaller than that of creating a new process.
2. Threads run in the process address space, so thread switching requires much less work from the operating system.
3. While waiting for slow I/O operations, you can also perform other computing tasks, and you can also overlap IO operations to improve performance. A process can wait for multiple io operations at the same time.
4. For computing-intensive applications, in order to run on multi-processors, the calculations are decomposed into multi-threaded processing.

Conceptual diagram of multithreading and virtual address translation

Let's take a look at virtual address translation:
the existence of virtual address space must require a page table to map the relationship between virtual addresses and physical addresses, and this table also needs to be loaded into memory. According to our understanding, in 32 Under the bit operating system, the address occupies 4 bytes, 32 bits, and all addresses have a total of 2^32 entries. Assume that there are only virtual addresses and physical addresses at the left and right ends of the page table, that is, one only occupies 8 bytes. Calculate it like this The page table of a process has 32 G. You must know that our computers generally only have 16 G. This method is obviously not feasible.

So the operating system uses a multi-level page table to solve this problem, that is, our address is divided into three parts, the first 10 bits, the middle 10 bits, and the last 12 bits.

The first 10 bits: [0 ~ 2^10-1]
The 10 bits in the positioning page directory: [0 ~ 2^10-1] The
last 12 bits: [0 ~ 4095] Determine the offset within the page

Explain the in-page offset:
When explaining the file system (disk file system), I said that the data storage in the disk is stored in blocks, generally 4kb, and the same physical memory should be divided according to this rule. : A page includes many page frames/page frames, each page frame is exactly 4kb, and 4kb is exactly 4096 bytes, combined with the start address in the page table entry, we can use the start address + page offset way to locate any address in physical memory.
1.1

Threads under the Linux operating system

In fact, there is no thread in the strict sense under the linux system, because the thread pcb of the linux system actually reuses the code of the process, so the native linux does not directly provide thread-related libraries. To use threads, you need to introduce a library: pthread library.

To use this library, you need to specify it at compile time:

myfile:lprocess.cpp
	g++ -o $@ $^ -std=c++11 -lpthread

create thread

原型:
	int pthread_create(pthread_t*thread, const pthread_attr_t *attr, void*(*start_routine)(void*), void* arg);
参数:
	thread:返回线程ID
	attr:设置线程属性,NULL表示使用默认属性
	start_routine:函数地址,线程启动后要执行的函数
	arg:传给线程启动函数的参数
返回值:
	成功返回0,失败返回错误码

thread terminated

There are three ways to terminate a thread without terminating the entire process:
1. Return from the thread function, which is equivalent to exit when the main function is called
2. The thread itself can call pthread_exit to terminate itself
3. A thread can call pthread_cancel to terminate another thread in the same process

pthread_exit:
	void pthread_exit(void* value_ptr)
参数:
	不能指向局部变量
返回值:
	无返回值线程结束时无法返回自身!

---------------------------------------------
pthread_cancel:
作用:取消一个正在执行的线程
	int pthread_cancel(pthread_t thread);
参数:
	thread:线程id
返回值:
	成功返回0;失败返回错误码。

thread wait

Why wait for threads?
1. The thread that has exited, its space has not been released, and it is still in the address space of the process.
2. Creating a new thread will not reuse the address space of the thread that just exited.

作用:等待线程结束
原型:
	int pthread_join(pthread_t thread,void** value_ptr)
参数:
	thread:线程id
	value_ptr:是个二级指针,指向一个指针,那个指针指向线程的返回值
返回值:
	成功返回0,失败返回错误码

Explanation:
The thread calling pthread_join will hang and wait until the corresponding thread is terminated. The thread is terminated in different ways, and the termination status obtained after waiting is different:
1. The thread thread returns through return, and value_ptr points to the return value of the thread thread.
2. The thread thread is terminated by cancel, and the value_ptr points to PTHREAD_CANCELED.
3. The thread thread is terminated by pthread_exit, then value_ptr points to the parameter passed to pthread_exit.
4. Setting it to NULL means that you are not interested in the termination status of the thread.

Thread id and thread address space layout

The pthread_create function will generate a thread id and store it in the address pointed to by the first parameter. If you print out this id, you will find that it is very large. In fact, this string of numbers is an address, and this address is a virtual address, as shown in the figure:

In this way, the temporary data generated by the main thread is pressed on the system stack, while other threads are stored in the stack provided by the pthread library.
insert image description here

lock (mutex)

We mentioned earlier that multiple threads can see the same address space. If we have a global variable, it will become very dangerous if all threads can change it at will. There will be concurrency issues.

At this time, the concept of lock is introduced:
in linux, this lock is called mutex

Why lock (no harm)

According to whether the function can be reentrant, it can be divided into non-critical area and critical area. The former can execute code concurrently, while the latter only allows one thread to execute.

Use an example to illustrate:
For example, now I want to insert two nodes, but only one step is completed when inserting the first node, so I switch to another process to insert 2, obviously this will cause problems.
insert image description here
Therefore, we have to lock some areas that do not allow multiple threads to access.

Mutex-related interface

Here are a few interfaces about locks:

互斥量的加锁和解锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

The realization principle of mutex

Suppose we define a variable:

int a = 100;

Now to operate it, there is only one instruction in our language, but it is actually three commands in the CPU's view:
insert image description here
if this process is interrupted, the above problems are prone to occur.
Let me introduce the swap and exchange commands first, the role: to exchange the contents of the register and the memory unit. Unlike the above three commands, the instructions provided by the architecture are atomic, that is, they are either executed or not executed.

insert image description here
Details:
1. The mutex here is in memory, which means that every thread can see it.
2. The exchange command is used here, which means that there will only be one key "1" for the lock.
3. When the thread is switched, it will record the data of this register and where it executes itself, that is: in the thread context.
4. The data will be recovered next time when it is the thread's turn.

In addition, the unlocking process is also atomic, so there is no need to worry about being switched halfway through unlocking.

Simulate implementing the thread interface

It should be noted that when implementing the run function interface, because we encapsulate a class, and the member functions of the class will have a this pointer by default, so runHelper cannot be passed to pthread_create. In order to be able to pass parameters, runHelper should be designed as static. But here will lead to a new problem, that is, static does not have this pointer and cannot call the member variable _func in the class, so we choose to pass the this pointer as the parameter of runHelper, and use a functor to call _func();

run logic diagram

参考代码 :
typedef void (*func_t)(void*);

class Thread
{
    
    
public:
    typedef enum
    {
    
    
        NEW=0,
        RUNNING,
        EXITED
    } ThreadStatus;
public:
    Thread(int num,func_t func,void* args)
        :_tid(0)
        ,_status(NEW)
        ,_args(args)
        ,_func(func)
    {
    
    
        char name[128] ;
        snprintf(name,sizeof(name),"thread-%d",num);
        _name = name;
    }

    int status()
    {
    
    
        return _status;
    }

    string threadname()
    {
    
    
        return _name;
    }

    pthread_t threadid()
    {
    
    
        if(_status != RUNNING)
            return _tid;
        else
        {
    
    
            cout << "no tid" << endl;
            return 0;
        }
            
    }

    //类的成员函数具有默认的参数this,参数不匹配
    //但没有了this指针,也无法直接访问类内成员函数和变量
    static void* runHelper(void* args)
    {
    
    
        //Thread* th = static_cast<Thread*> args;
        Thread* th = (Thread*) args;
        (*th)();
        return nullptr;
    }

    //仿函数
    void operator()()
    {
    
    
        _func(_args);
    }

    void run()
    {
    
     
        int n = pthread_create(&_tid,nullptr,runHelper,this);
        if(n != 0) exit(1);
        _status = RUNNING;

    }

    void join()
    {
    
    
        int n = pthread_join(_tid,nullptr);
        if(n != 0)
        {
    
    
            cerr << "main thread join thread" << _name << endl;
        }
    }

    ~Thread()
    {
    
    

    }
private:
    pthread_t _tid;
    string _name;
    func_t _func;//将来要回调的函数
    void* _args;  //喂给func的参数
    ThreadStatus _status;
};

Analog implementation of thread locks (RAII style)

The RAII-style lock encapsulates the mutex into a class, and locks the entire area with one statement. RAII-style locks are more elegant, clever use of the encapsulation features.

//自己不维护锁,外部传入
class Mutex
{
    
    
public:
    Mutex(pthread_mutex_t* mutex)
        :_pmutex(mutex)
    {
    
    

    }

    ~Mutex()
    {
    
    

    }

    void lock()
    {
    
    
        pthread_mutex_lock(_pmutex);
    }

    void unlock()
    {
    
    
        pthread_mutex_unlock(_pmutex);
    }
public:
    pthread_mutex_t* _pmutex;
};


class LockGuard
{
    
    
public:
    LockGuard(pthread_mutex_t* mutex)
        :_mutex(mutex)
    {
    
    
        _mutex.lock();
    }

    ~LockGuard()
    {
    
    
        _mutex.unlock();
    }
public:
    Mutex _mutex;
};

In this way, we only need to define such a temporary object in the place we want to protect. When the object is created, it will be automatically locked. When the same thread finishes executing the code in this critical section, the object corresponding to the code will automatically call the destructor to unlock.
insert image description here

epilogue

The above is the whole content of the multi-threading part, here is the blue scholar, thank you for watching, see you next time~

Guess you like

Origin blog.csdn.net/m0_73209194/article/details/130873777