[Linux] Thread Safety - Supplement | Mutex, Lock | Synchronization, Condition Variables

1. Knowledge Supplement

thread ID

pthread_create creates a thread and generates a thread ID stored in the first parameter, which is not the same as the LWP in the kernel. The first parameter of the pthread_create function points to a virtual memory unit. The address of the memory unit is the ID of the newly created thread. This ID is the category of the thread library, and the LWP in the kernel is the category of process scheduling. The lightweight process is the smallest OS scheduling Unit, a numeric value is required to represent this unique thread.

Linux does not provide real threads, only LWP, but programmers and users only need threads regardless of LWP. Therefore, the OS has designed a native thread library between the OS and the application program, the pthread library, and the system saves the LWP. There may be multiple threads in the native thread library, which can be used by others at the same time. The OS only needs to manage the kernel execution flow LWP, and other data such as the thread interface used by the user needs to be managed by the thread library itself. Therefore, the thread library needs to "describe first, then organize" thread management.

The thread library is actually a dynamic library:

image-20230331120556954

When the process is running, the dynamic library is loaded into the memory, and then mapped to the shared area of ​​the process address space through the page table. At this time, all threads of the process can see the dynamic library:

image-20230331121415523

每个线程都有自己独立的栈: The stack used by the main thread is the original stack in the process address space, and the other threads use the stack in the shared area . Each thread has its own struct pthread, which contains the attributes of the corresponding thread. Each thread also has its own thread. Local storage (adding __thread can set a built-in type as thread local storage), including the context when the corresponding thread is switched. Each new thread has an area in the shared area to describe it, so to find a user-level thread, we only need to find the starting address of the thread memory block to obtain the thread information:

image-20230331122833069

The thread function starts by operating the thread attributes inside the library, and finally hands over the code to be executed to the corresponding kernel-level LWP for execution. Therefore, the essence of thread data management is in the shared area.

The essence of the thread ID is a virtual address on the shared area of ​​the process address space:

void* start_routine(void*args)
{
    while(true)
    {
        printf("new thread tid:%p\n",pthread_self());
        sleep(2);
    }
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,nullptr);
    while(true)
    {
        printf("main thread tid:%p\n",pthread_self());
    }
    return 0;
}

image-20230331134341468

Local Storage Validation

Give a global variable g_val, let a thread perform ++, other threads will be affected:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int g_val = 100;
void* start_routine(void*args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        cout<<name<<" running ... "<<"g_val: "<<g_val<<"&g_val: "<<&g_val<<endl;
        sleep(1);
        ++g_val;
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
    while(true)
    {
        printf("main thread g_val: %d &g_val: 0x%x\n",g_val,&g_val);
        sleep(1);
    }
    pthread_join(tid,nullptr);
    return 0;
}

image-20230404232354409

At this time, add __thread to the global variable g_val:

image-20230404232623125

From the output results, g_val is no longer shared at this time, and is unique to each thread. Added __threadthe ability to set a built-in type as thread-local storage. Each thread has a thread-specific attribute between global variables and local variables.

Thread.hpp - encapsulation of threads

If we want to use it like C++, when creating and using threads, we can directly construct the object and set the callback function, and we can simply encapsulate the thread native interface:

#include <iostream>
#include <pthread.h>
#include <cassert>
#include <cstring>
#include <functional>
class Thread;
//上下文
class Context
{
public:
    Thread *this_;
    void *args_;
public:
    Context():this_(nullptr),args_(nullptr)
    {}
    ~Context()
    {}
};
class Thread
{
public:
    typedef std::function<void*(void*)> func_t;
    const int num = 1024;
public:
    Thread(func_t func,void*args,int number):func_(func),args_(args)
    {
        char buffer[num];
        snprintf(buffer,sizeof(buffer),"thread-%d",number);
        name_=buffer;
        Context*ctx = new Context();
        ctx->this_ = this;
        ctx->args_=args_;
        int n = pthread_create(&tid_,nullptr,start_routine,ctx);
        assert(n==0);
        (void)n;
    }
    static void*start_routine(void*args)
    {
        Context*ctx = static_cast<Context*>(args);
        void*ret = ctx->this_->run(ctx->args_);
        delete ctx;
        return ret;
    }
    // void start()
    // {
    //     Context*ctx = new Context();
    //     ctx->this_ = this;
    //     ctx->args_=args_;
    //     int n = pthread_create(&tid_,nullptr,start_routine,ctx);
    //     assert(n==0);
    //     (void)n;
    // }
    void join()
    {
        int n = pthread_join(tid_,nullptr);
        assert(n==0);
        (void)n;
    }
    void*run(void*args)
    {
        return func_(args);
    }

    ~Thread()
    {}

private:
    std::string name_;
    pthread_t tid_;
    func_t func_;
    void* args_;
};

main.cc

void* thread_run(void* args)
{
    std::string name = static_cast<const char *>(args);
    while(true)
    {
        cout << name << endl;
        sleep(1);
    }
}
int main()
{
    std::unique_ptr<Thread> thread1(new Thread(thread_run,(void*)"hellothread",1));
    std::unique_ptr<Thread> thread2(new Thread(thread_run,(void*)"COUTthread",2));
    std::unique_ptr<Thread> thread3(new Thread(thread_run,(void*)"PRINTthread",3));

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

2. Thread safety issues

The global variable g_val can be accessed by multiple threads at the same time, and it is a shared resource that can be accessed by multiple threads. When multiple threads operate on it, problems may occur:

The following simulates the process of grabbing tickets, the process of multiple threads doing – to the shared resource tickets:

#include "Thread.hpp"
using std::cout;
using std::endl;
//共享资源
int tickets = 1000;
void* get_ticket(void* args)
{
    std::string name = static_cast<const char *>(args);
    while(true)
    {
        if(tickets>0)
        {
            usleep(1234);
            cout<<name<<"正在抢票 : "<<tickets<<endl;
            tickets--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}
int main()
{
    std::unique_ptr<Thread> thread1(new Thread(get_ticket,(void*)"hellothread",1));
    std::unique_ptr<Thread> thread2(new Thread(get_ticket,(void*)"COUTthread",2));
    std::unique_ptr<Thread> thread3(new Thread(get_ticket,(void*)"PRINTthread",3));
    std::unique_ptr<Thread> thread4(new Thread(get_ticket,(void*)"TESTthread",4));

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

image-20230405001609431

At this time, the result has a negative number. In real life, how can there be a negative number for ticket grabbing?

The result is negative:

If negative numbers need to appear: let multiple threads cross execution as much as possible, the essence of multiple threads cross execution: let the scheduler occur thread scheduling and switching as frequently as possible

When does a thread generally switch threads : 时间片到了或者来了更高优先级的线程或者线程等待的时候.

When does the thread detect the above problem: when returning from the kernel state to the user state, the thread must detect the scheduling state, and if possible, thread switching occurs directly

Negative tickets are due to the cross-execution of multiple threads, the essence of cross-execution of multiple threads: let the scheduler schedule and switch threads as frequently as possible

When tickets==1 , all processes can enter, and then judge: 1. Read the memory data in the register in the cpu 2. Make a judgment; the first thread judgment is greater than 0, and the thread will be cut off at this time , there is only one register, and the content of the register is the context of the current execution flow, which will take the context away, before there is time for tickets–, the tickets seen by other threads are also 1, and their own context must be saved...the thread will sleep before the ticket is reduced or reduced After a while, when a thread wakes up tickets–

--的本质就是1.读取数据2.更改数据3.写回数据

It is not safe to make multithreaded changes to a global variable:

Perform ++ or – on variables, it seems that there is only one statement in C and C++, but there are at least three statements after assembly:

1. Read data from the memory to the CPU register 2. Let the CPU perform corresponding arithmetic operations in the register 3. Write back the new result to the location of the variable in the memory

Now thread 1 loads the data into the register, do –, becomes 999, unfortunately it is cut away when it is written back to the memory in the third step, and the context is also taken away by the way:

image-20230419193926618

At this time, thread 2 is scheduled, and thread 2 is very happy. It has been - until 1tickets becomes 100, and the variable in memory also becomes 100, but when it wants to continue -, thread 2 times cuts away, with My own context is gone, and now thread 1 is back: restore the context and continue to the third step before. At this time, thread 2 finally changed the tickets to 100, but it was changed to 999 by thread 1

image-20230419194254020

It became 999 again, causing disruption

image-20230419194457726

It can be seen from this that the global variables we define are often unsafe when they are not protected. Like the above example, data security problems occur when multiple threads are executed alternately 数据不一致问题.

The solution to this problem is to add locks!


Three, Linux thread mutual exclusion

Mutual exclusion related concepts

Critical resources : shared resources that are accessed safely by multiple execution flows are called critical resources

Critical section : Code that accesses critical resources with multiple execution flows is a critical section

Mutual exclusion: At any time, mutual exclusion guarantees that one and only one execution flow enters the critical section , accesses critical resources, and usually protects critical resources.

Atomicity: An operation that will not be interrupted by any scheduling mechanism. The operation has only two states, either do not do it or finish it. This is atomicity.

Now let’s briefly understand atomicity: if the operation of a resource can be completed with only one assembly statement, it is atomic, otherwise it is not atomic.

For variables ++ or –. On C and C++, it seems that there is only one statement, but after assembly, there are at least three statements:

1. Read data from memory to CPU register

2. Let the CPU perform corresponding arithmetic and logic operations in the register

3. Write back the new result to the location of the variable in memory

When accessing a resource, either do not do it or finish it, it is not atomic: thread A is switched, it is not finished, there is an intermediate state, it is not atomic.

In fact, when doing – on variables, it corresponds to three assembly statements, and will correspond to three assembly statements in the future! So obviously, ++, – is not atomic, not a statement.

Simple ++ or ++ is not atomic, and there may be data consistency problems.

mutex

In most cases, the data used by the thread is a local variable, and the address space of the variable is in the thread stack space. In this case, the variable belongs to a single thread, and other threads cannot obtain this variable.

But sometimes, many variables need to be shared between threads. Such variables are called shared variables, and the interaction between threads can be completed through data sharing.

Multiple threads concurrently operate shared variables, which will cause problems: data inconsistency

To resolve thread-unsafe situations, protect shared resources:

The code must have mutual exclusion behavior: when the code enters the execution of the critical section, no other thread is allowed to enter the critical section.

If multiple threads request to execute the code in the critical section at the same time, and no thread is executing in the critical section at this time, only one thread is allowed to enter the critical section.

If a thread is not executing in a critical section, that thread cannot prevent other threads from entering the critical section.

In fact, a lock is needed. The lock provided by Linux is called a mutex. If a thread holds the lock, other threads cannot come in and access it.

Common related interfaces:

#include <pthread.h>
// 初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
// 销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 全局
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//成功返回0,失败返回错误码

It can be defined locally or globally.

Pthread_mutex_t is the type of lock. If the lock we define is global, don't initialize and destroy it with pthread_mutex_int and pthread_mutex_destroy.

#include <pthread.h>
//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);

//如果加锁成功,直接持有锁,加锁不成功,此时立马出错返回(试着加锁,非阻塞获取方式)
int pthread_mutex_trylock(pthread_mutex_t *mutex);

//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 成功返回0,失败返回错误码

The use of mutex

Use of global locks

Code using global lock + 4 threads:

Define a global lock and initialize PTHREAD_MUTEX_INITIALIZER, and use pthread_create to create 4 threads for testing. Since the lock is global at this time, we don't need to pass the lock to each thread:

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using std::cout;
using std::endl;
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using std::cout;
using std::endl;
int tickets = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void* get_ticket(void* args)
{
    std::string username = static_cast<const char *>(args);
    while(true)
    {
        pthread_mutex_lock(&lock);
        if(tickets>0)
        {
            usleep(11111);
            cout<<username<<"正在抢票 : "<<tickets<<endl;
            tickets--;
            pthread_mutex_unlock(&lock);
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr,  get_ticket, (void *)"thread 1");
    pthread_create(&t2, nullptr,  get_ticket, (void *)"thread 2");
    pthread_create(&t3, nullptr,  get_ticket, (void *)"thread 3");
    pthread_create(&t4, nullptr, get_ticket, (void *)"thread 4");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    return 0;
}

image-20230419204523501

Use of local locks

Code for local lock + for loop to create threads:

At this time, the lock is local. In order to pass the lock to each thread, we can define a structure ThreadData, which stores the thread name and lock:

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
using std::cout;
using std::endl;
int tickets = 1000;
class ThreadData
{
public:
    ThreadData(const std::string&threadname,pthread_mutex_t *mutex_p)
        :threadname_(threadname),mutex_p_(mutex_p)
    {}
    ~ThreadData(){}
public:
    std::string threadname_;
    pthread_mutex_t *mutex_p_;
};
void* get_ticket(void* args)
{
    ThreadData*td = static_cast<ThreadData *>(args);
    while(true)
    {
        pthread_mutex_lock(td->mutex_p_);
        if(tickets>0)
        {
            usleep(11111);
            cout<<td->threadname_<<"正在抢票 : "<<tickets<<endl;
            tickets--;
            pthread_mutex_unlock(td->mutex_p_);
        }
        else
        {
            pthread_mutex_unlock(td->mutex_p_);
            break;//注意这里有break
        }
    }
    return nullptr;
}
int main()
{
 #define NUM 4
     pthread_mutex_t lock;
     pthread_mutex_init(&lock,nullptr);
     std::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,get_ticket,td);
     }
     for(const auto&tid:tids)
     {
        pthread_join(tid,nullptr);
     }
    pthread_mutex_destroy(&lock);
    return 0;
}

image-20230419204133974

At this time, the running result can be reduced to 1 every time, but the running speed is also slower. This is because the process of locking and locking is executed serially by multiple threads, and the program slows down

At the same time, it can be seen here that only one thread is grabbing tickets every time . This is because the lock only stipulates mutually exclusive access, and does not stipulate who will execute first, so whoever has the most competitive power will hold the lock.

To solve this problem: Think about it, is it over after grabbing tickets? In real life, there are still some tasks to be done after grabbing tickets: such as sending orders

image-20230419204953465

image-20230419205004976

So far, the problem of grabbing tickets has been solved.

Understanding of mutex

  • look at the lock

The lock itself is a shared resource! Global variables must be protected, locks are used to protect global resources, and locks themselves are also global resources. Who will protect the security of locks?

pthread_mutex_lock, pthread_mutex_unlock: The process of locking and unlocking must be safe! The locking process is actually atomic

Whoever holds the lock enters the critical section!

If the application is successful, continue to execute backwards. If the application is not successful for the time being, what will happen to the execution flow: lock for the first time, and then add a lock again: what will happen to the result:

image-20230419205806708

Running at this time, the program is not executing, and the execution flow will be blocked!

image-20230419210512025

pthread_mutex_trylock: try to lock, if the lock is successful, it will hold the lock, if the lock is not successful, it will return an error immediately

Generally, this kind of lock is called a pending lock: if the application is temporarily unsuccessful, the execution flow will be blocked, waiting for the lock to be released successfully!

  • Atomic concept understanding

image-20230419210932013

If thread 1 successfully applies for the lock and enters the critical resource, while accessing the critical resource, other threads are doing: blocking and waiting

If thread 1 successfully applies for the lock and enters the critical resource, I can be switched while accessing the critical resource! ! Absolutely

When the thread holding the lock is cut away, it is cut away holding the lock. Even if it is cut away, other threads still cannot apply for the lock successfully, and they cannot execute successively! Until I finally release this lock!

Therefore, for other threads, there are no more than two meaningful lock states: 1. Before applying for the lock 2. After releasing the lock

From the perspective of other threads, the process of viewing the lock held by the current thread is atomic

结论

**When we use locks in the future: we must try our best to ensure that the granularity of the critical section is very small (granularity: how much protection code is in the middle of the lock) **Note: Locking is a programmer's behavior, and it must be done if it is added. add! (Public resources, either lock or not, this is the programmer's behavior, don't write BUG)!

The essence of locking and unlocking : the locking process is atomic! After locking, the execution flow must be unlocked in the future 一个.

  • Mutex realizes the principle of atomicity

Simple i++, ++i are not atomic, which will lead to data inconsistency

Talking about locking from assembly: In order to realize mutual exclusion lock operation, most architectures provide swap and exchange instructions, which are used to directly exchange the data of registers and memory units. Since only one instruction is used, atomicity can be guaranteed .

image-20230419211948096

加锁

image-20230419212503660

The instructions here xchgbcan directly exchange the data in the cpu with the data in the memory :

image-20230419213809180

image-20230419214158235

解锁: The process is very simple, move the content of the register 1 to the memory, return directly, and the unlocking is completed

Mutex.hpp - the package of mutex

If we want to use it simply, how should we carry out the packaging design - make a simple design, pass in a lock to automatically lock and unlock for us, RAII style lock

Mutex.hpp

//Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
    Mutex(pthread_mutex_t *lock_p=nullptr):lock_p_(lock_p)
    {}
    
    void lock()
    {
        if(lock_p_) pthread_mutex_lock(lock_p_);
    }

    void unlock()
    {
        if(lock_p_) pthread_mutex_unlock(lock_p_);
    }
    ~Mutex()
    {

    }
private:
    pthread_mutex_t *lock_p_;
};
class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):mutex_(mutex)
    {
        mutex_.lock();//在构造函数中加锁
    }
    ~LockGuard()
    {
        mutex_.unlock();//在析构函数中解锁
    }

private:
    Mutex mutex_;
};

main.cc

using std::cout;
using std::endl;
int tickets = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *get_ticket(void *args)
{
    std::string username = static_cast<const char *>(args);
    while (true)
    {
        {//代码块,不给usleep加锁
            LockGuard lockguard(&lock);
            if (tickets > 0)
            {
                usleep(1111);
                cout << username << "正在抢票 : " << tickets << endl;
                tickets--;
            }
            else
            {
                break;
            }
        }
        usleep(1000);
    }
    return nullptr;
}
int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr, get_ticket, (void *)"thread 1");
    pthread_create(&t2, nullptr, get_ticket, (void *)"thread 2");
    pthread_create(&t3, nullptr, get_ticket, (void *)"thread 3");
    pthread_create(&t4, nullptr, get_ticket, (void *)"thread 4");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    pthread_join(t4, nullptr);
    return 0;
}

Reentrant vs thread safe

Reentrant : The same function is called by different execution flows. Before the execution of the current process is completed, other execution flows enter again. We call it reentrant.

In the case of reentrancy, if a function does not have any different results or any problems , then the function is called a reentrant function , otherwise, it is a non-reentrant function

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

Thread safety is not necessarily reentrant , but reentrant functions must be thread safe

If the access to the critical resource is locked, the function is thread-safe, but if the reentrant function has not released the lock, it will cause a deadlock, so it is not reentrant

Common thread-safe situations:

Each thread has only read access to global variables or static variables, but no write access. Generally speaking, these threads are safe

Classes or interfaces are atomic operations for threads

Switching between multiple threads will not cause ambiguity in the execution results of this interface

Common non-reentrancy cases

The malloc/free function is called, because the malloc function uses a global linked list to manage the heap

Calls to standard I/O library functions, many implementations of the standard I/O library use global data structures in a non-reentrant manner

Static data structures are used in reentrant function bodies

deadlock

A group of execution flows (regardless of processes or threads) hold their own lock resources, but also want to apply for each other's locks. Locks are not preemptible (unless they actively return them), which will cause multiple execution flows to wait for each other's resources. As a result, the code cannot be advanced. this is deadlock

ps: A lock can cause a deadlock. We wrote about it when grabbing tickets. Adding a lock can cause a deadlock.

Derivation chain: why there is a deadlock: you must have used a lock - the lock ensures the safety of critical resources, multi-threaded access we may have data inconsistency problems - multi-threaded, global resources - most of the multi-threaded resources (global ) is shared—the multi-threaded feature, which brings new problems while solving problems: deadlock, any technology has its own boundaries, and it is bound to introduce new problems while solving problems

Deadlock four necessary conditions:

1. Mutual exclusion : a shared resource is used by an execution flow at a time

2. Request and hold : An execution flow is blocked due to requesting resources, and the existing resources are kept

3. No deprivation : the resources obtained by an execution flow cannot be forcibly deprived before they are used up

4. Loop waiting condition : a loop problem is formed between execution flows, and the loop waits for resources

To avoid deadlock, 1. The four necessary conditions for destroying deadlock 2. The order of locking is consistent 3. Avoid the scene where the lock is not released 4. One-time allocation of resources

Deadlock avoidance algorithm (understand): deadlock detection algorithm, banker's algorithm


Four, Linux thread synchronization

Introduce some scenarios: VIP in the study room, first come first served, locked when going to the toilet, no one else can get in, close to resources and strong competitiveness, always yourself, repeatedly put the key and take the key, causing other people to be hungry; another example is grabbing Ticket system We see that a thread has been grabbing tickets continuously, causing starvation of other threads. In order to solve this problem: we let these threads visit in a certain order under the condition of data security, which is线程同步

饥饿状态: Threads that cannot obtain lock resources and cannot access public resources are in a starvation state. But it's not wrong, but it's unreasonable

竞态条件: Because of timing problems, the program is abnormal, which is called a race condition.

线程同步: 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

condition variable

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, the condition variable

Condition variables are usually used in conjunction with mutexes.

The use of condition variables: one thread waits for the condition of the condition variable to be established and is suspended; another thread wakes up the waiting thread after the condition is established.

condition variable interface

#include <pthread.h>
//初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//销毁
int pthread_cond_destroy(pthread_cond_t *cond);
#include <pthread.h>
//特定时间阻塞等待
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex,
       const struct timespec *restrict abstime);
//等待
int pthread_cond_wait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex);
#include <pthread.h>
// 唤醒一批线程
int pthread_cond_broadcast(pthread_cond_t *cond);
// 唤醒一个线程
int pthread_cond_signal(pthread_cond_t *cond);

Understanding Condition Variables

For example: the company is recruiting: applicants need to interview, and everyone can't enter the room for interview at the same time, but there is no organization. After the interview with the previous person, the interviewer opens the door to prepare for the next interview. A group of people are waiting for the interview outside. But some people can't grab others, there are too many people, and the interviewer can't remember who has interviewed, so it is possible that one person will go to the interview after the interview, causing other people to be hungry, and the efficiency is very low at this time

Later, HR re-managed: set up a waiting area, first line up to the waiting area for an interview, and now everyone is queuing up and has a chance to interview, and this waiting area is a condition variable. If a person wants to interview, he has to go first Waiting in the waiting area, all future applicants have to go to the condition variable, etc.

image-20230409143410229

条件不满足的时候,线程必须去某些定义好的条件变量上进行等待

The condition variable (struct cond, structure) contains state and queue, and the condition variable we defined contains a queue, and threads that do not meet the conditions are linked to this queue to wait.

image-20230419235917050

Use of condition variables

Control the execution of threads through condition variables

条件变量本身不具备互斥的功能, so the condition variable must be used with a mutex:

  • wake up one thread at a time

Create 2 threads and wake up one thread per second (or wake up all) through the condition variable:

int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
        //判断省略
        cout<<name<<" -> "<<tickets<<endl;
        tickets--;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
    pthread_t t1,t2;
    pthread_create(&t1,nullptr,start_routine,(void*)"thread 1");
    pthread_create(&t1,nullptr,start_routine,(void*)"thread 2");

    while(true)
    {
        sleep(1);
        pthread_cond_signal(&cond);
        cout<<"main thread wakeup one thread..."<<endl;
    }
    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);

    return 0;
}

image-20230420000722967

The main thread calls one by one, and outputs and prints in a certain order.

  • Wake up a large number of threads at once
int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
        //判断省略
        cout<<name<<" -> "<<tickets<<endl;
        tickets--;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
    pthread_t t1,t2;
    pthread_t t[5];
    for(int i = 0;i<5;i++)
    {
        char*name = new char[64];
        snprintf(name,64,"thread %d",i+1);
        pthread_create(t+i,nullptr,start_routine,name);
    }
    while(true)
    {
        sleep(1);
        pthread_cond_broadcast(&cond);
        cout<<"main thread wakeup one thread..."<<endl;
    }
    for(int i = 0;i<5;i++)
    {
        pthread_join(t[i],nullptr);
    }
    return 0;
}

image-20230420001539170

Guess you like

Origin blog.csdn.net/weixin_60478154/article/details/130256805