C++ multithreading learning 09 conditional variables

1. Condition variables

Take the producer-consumer model in the previous section as an example, and its consumers look like this:

void XMsgServer::Main()
{
    
    
    while (!is_exit())
    {
    
    
        sleep_for(10ms);
        unique_lock<mutex> lock(mux_);
        if (msgs_.empty())
            continue;
        while (!msgs_.empty())
        {
    
    
            //消息处理业务逻辑
            cout << "recv : " << msgs_.front() << endl;
            msgs_.pop_front();
        }

    }
}

There are problems with such a producer-consumer model:
if the consumer is kept waiting for the producer to produce, it will occupy CPU resources. If sleep is added after each cycle, the producer may miss the point in time when the producer just finishes production. Therefore, it is impossible to finely control the sleep time in terms of time, so there needs to be a way to not occupy the cpu when the producer is not producing well, and block there until the production is complete, so we need to introduce condition variables.
Condition variable definition:


#include <condition_variable>
condition_variable cv;

The difference between a condition variable and a semaphore:
the semaphore has value, and the resource value will be +1 after each v operation, and the resource quantity is checked before each P, and the
condition variable is not valued until the resource quantity >= 0. Yes, only the function of queuing is realized

Two operations of condition variables:
1. wait function, wait() can be split into three operations in turn: release the mutex, wait for the condition variable to send a signal to wake up the current thread (at this time the current thread has been blocked), when the signal is received When the mutex is acquired again , it has two usages:
(1) wait (unique_lock & lck)

Execution of the current thread will be blocked until notify is received.

(2)wait(unique_lock &lck,Predicate pred)

The current thread is only blocked when pred=false; if pred=true, it is not blocked.

2. The wake-up operation also has two usages:
notify_one(): Because only the first thread in the waiting queue is woken up; there is no lock contention, so the lock can be obtained immediately. The rest of the threads will not be woken up and need to wait to call notify_one() or notify_all() again.

notify_all(): It will wake up all threads blocked in the waiting queue, there is lock contention, and only one thread can acquire the lock. The remaining threads that have not acquired the lock will continue to try to acquire the lock (similar to polling) without blocking again. When the thread holding the lock releases the lock, one of those threads acquires the lock. The rest will then try to acquire the lock.

Two, write thread

01. Prepare condition variable
02. Lock with mutex
03. Unlock after production is complete
04. Condition variable sends signal

void ThreadWrite()
{
    
    
    for (int i = 0;;i++)
    {
    
    
        stringstream ss;
        ss << "Write msg " << i;
        unique_lock<mutex> lock(mux);
        msgs_.push_back(ss.str());
        lock.unlock();
        cv.notify_one(); //发送信号
        //cv.notify_all();
        this_thread::sleep_for(3s);
    }
}

Generally, cv.notify_all() notifies all threads to exit when the process exits

Three, read the thread

01 Obtain the mutex common to the thread that changes the shared variable.
02 The condition variable cv waits for the signal notification through wait(), and remains blocked until it receives the notification , which will not occupy resources or delay.
After entering wait(), it will be unlocked first, and then To try to get a signal,
two expressions of wait():

void wait(unique_lock<mutex>& _Lck) {
    
     // wait for signal
// Nothing to do to comply with LWG‐2135 because std::mutex lock/unlock are
nothrow
_Check_C_return(_Cnd_wait(_Mycnd(), _Lck.mutex()>_Mymtx()));
}
template <class _Predicate>
void wait(unique_lock<mutex>& _Lck, _Predicate _Pred) {
    
     // wait for signal and test
predicate
while (!_Pred())
{
    
     wait(_Lck);
}
}

No lamda expression is used: when the signal is received, the lock will be locked immediately, and then the following code is run to be thread-safe. Using
lamda expression: when the expression returns true after receiving the signal, run the following code and return false Continue to block (here is to run the following code when the queue is not empty, and continue to block when it is empty), and then let other consumers judge
03 to lock and start consumption after obtaining the signal

void ThreadRead(int i)
{
    
    
    for (;;)
    {
    
    
        cout << "read msg" << endl;
        unique_lock<mutex> lock(mux);
        //cv.wait(lock);//解锁、阻塞等待信号
        cv.wait(lock, [i] 
            {
    
    
                cout << i << " wait" << endl;
                return !msgs_.empty(); 
            });
        //获取信号后锁定
        while (!msgs_.empty())
        {
    
    
            cout << i << " read " << msgs_.front() << endl;
            msgs_.pop_front();
        }
    }
}

Three, main

The producer thread generates and puts a piece of data into the queue every three seconds, sends a signal with a condition variable, and then detach the producer thread without managing its thread resources to
generate three consumer threads, and each thread also does detach() ,

int main(int argc, char* argv[])
{
    
    
    thread th(ThreadWrite);
    th.detach();
    for (int i = 0; i < 3; i++)
    {
    
    
        thread th(ThreadRead, i + 1);
        th.detach();
    }
    getchar();
    return 0;
}

insert image description here

4. Change msgserve to condition variable version

After the message is generated, you can respond immediately, unlike before, you have to constantly judge whether the current queue is not empty in the while: the
previous msgserver:

void XMsgServer::Main()
{
    
    
    while (!is_exit())
    {
    
    
        sleep_for(10ms);
        unique_lock<mutex> lock(mux_);
        if (msgs_.empty())
            continue;
        while (!msgs_.empty())
        {
    
    
            //消息处理业务逻辑
            cout << "recv : " << msgs_.front() << endl;
            msgs_.pop_front();
        }

    }
}

Condition variable version: Don't forget the this pointer, when the thread exits, it should also notify wait that it is time to exit

void XMsgServer::Main()
{
    
    
    while (!is_exit())
    {
    
    
        //sleep_for(10ms);
        unique_lock<mutex> lock(mux_);
        cv_.wait(lock, [this] 
            {
    
    
                cout << "wait cv" << endl;
                return !msgs_.empty(); 
            });
        while (!msgs_.empty())
        {
    
    
            //消息处理业务逻辑
            cout << "recv : " << msgs_.front() << endl;
            msgs_.pop_front();
        }

    }
}

Sending messages to consumers also needs to be changed: first unlock, then send a notification to a consumer

void XMsgServer::SendMsg(std::string msg)
{
    
    
    unique_lock<mutex> lock(mux_);
    msgs_.push_back(msg);
    lock.unlock();
    cv_.notify_one();
}

The stop of the consumer thread also needs to be modified. At this time, all consumer threads should be signaled (although there is only one consumer process in this example), if not all consumer threads are signaled

void XMsgServer::Stop()
{
    
    
    is_exit_ = true;
    cv_.notify_all();
    Wait();
}

result:
insert image description here

It can be found that it is finally stuck in the cout of the lamda expression of the cv expression. This is because the queue is not empty after entering the CV wait for the last time, and !msgs_.empty(); returns false, which will block here. Therefore, when the thread exits is_exit=TRUE, it will no longer judge !msgs_.empty();, but directly return true to execute the code after the cv expression until the thread ends:

void XMsgServer::Main()
{
    
    
    while (!is_exit())
    {
    
    
        //sleep_for(10ms);
        unique_lock<mutex> lock(mux_);
        cv_.wait(lock, [this] 
            {
    
    
                cout << "wait cv" << endl;
                if (is_exit())return true;
                return !msgs_.empty(); 
            });
        while (!msgs_.empty())
        {
    
    
            //消息处理业务逻辑
            cout << "recv : " << msgs_.front() << endl;
            msgs_.pop_front();
        }

    }
}

Exited normally:
insert image description here
ps, if there is no notify all when calling stop, it will also be stuck here:
insert image description here
this is because the consumer's cv.wait() is always in the second step, that is, waiting for information in a blocked state. before continuing to execute the following code

Guess you like

Origin blog.csdn.net/qq_42567607/article/details/126060425