[c++] Lock mechanism: mutex and condition_variable description and sample code

Lock mechanism description

mutex

mutex lock
Used to protect resources under multi-threading. Only one thread can read and write at the same time.

std::mutex is the mutex lock (Mutex) class provided in the C++ standard library, which is used to implement mutually exclusive access between multiple threads.
It provides two main operations: lock() and unlock().
When a thread calls the lock() function, if the lock is not held by another thread, then the thread will acquire the lock.
Otherwise, it blocks until the lock is released. And when a thread calls the unlock() function, it releases the lock it acquired previously.

condition_variable

condition variable
Used for synchronization operations under multi-threads: A->B->A->B

std::condition_variable is the condition variable (Condition Variable) class provided in the C++ standard library.
Condition variables are a synchronization mechanism between threads.
It allows a thread to wait under a certain condition, and other threads can notify the waiting thread to continue execution when this condition is met.

Lock code in consumer producer mode

std::mutex mutex_; // Define mutex lock
std::condition_variable cond_; // Define condition variable

void producer() {<!-- -->
    // Production Data
    {<!-- -->
        std::unique_lock<std::mutex> lock(mutex_);
        // Production Data
    } // Automatically release the lock when leaving the scope

    cond_.notify_one(); // Notify a waiting consumer thread
}

void consumer() {<!-- -->
    std::unique_lock<std::mutex> lock(mutex_);
    cond_.wait(lock); // Wait for condition variable notification, release the lock at the same time, and reacquire the lock when notified

    //Consumption data
}

Why two variables are needed

  1. std::mutex (mutex lock): std::mutex is used to protect shared resources and ensure that only one thread can access shared resources at any time. When a thread needs to modify a shared resource, it must first lock the mutex to ensure that no other thread modifies the resource at the same time. In the above example, std::mutex is used to protect the reading and writing of shared data and prevent race conditions caused by multiple threads modifying data at the same time.

  2. std::condition_variable (condition variable): std::condition_variable provides a mechanism that allows a thread to wait when a specific condition is met, while allowing other threads to notify the waiting thread to continue execution when the condition is met. In the above example, std::condition_variable is used to establish communication between threads. When a thread is waiting for a certain condition to be true, it will actively give up the lock through the wait() function of std::condition_variable and enter the waiting state. At a certain moment, another thread notifies the waiting thread that the conditions have been met through the notify_one() or notify_all() function of the condition variable and execution can continue.

C++11 new features lock_guard and unique_lock

lock_guard

Automatically lock and unlock within scope
No need to manually manage lock release

scenes to be used:

  • When mutually exclusive access to a resource within a certain scope is required, std::lock_guard can be used. Because its life cycle and scope are relatively small, it is more lightweight

Sample code:

std::mutex mutex_;
{<!-- -->
    std::lock_guard<std::mutex> lock(mutex_); // Automatically lock and unlock within the scope
    //Perform operations that require mutually exclusive access
} // Automatically release the lock when the scope ends

unique_lock

Automatically release the lock at the end of the scope
Manually control the locking and unlocking of locks within the scope

scenes to be used:

  • When you need to more flexibly control the life cycle of the lock, or need to execute some code that may cause exceptions during the lock, you can choose to use std::unique_lock

Sample code:

std::mutex mutex_;
{<!-- -->
    std::unique_lock<std::mutex> lock(mutex_); // Manually control the locking and unlocking of the lock
    //Perform operations that require mutually exclusive access
    lock.unlock(); // Manually release the lock
    //Perform operations that do not require mutually exclusive access
} // Automatically release the lock when the scope ends

Lock queue example demo

avpacketqueue.h

#ifndef AVPACKETQUEUE_H
#define AVPACKETQUEUE_H
#include <mutex>
#include <condition_variable>
#include <queue>
#include <list>

#ifdef __cplusplus
extern "C"
{<!-- -->
// Contains ffmpeg header files
//#include "libavutil/avutil.h"
//#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
}
#endif

/*
    packet queue
    - The underlying structure uses a double linked list
*/

class AVPacketQueue
{<!-- -->
public:
    AVPacketQueue();
    ~AVPacketQueue();

    //Enter the queue
    int Push(AVPacket *val);
    //dequeue
    AVPacket *Pop(const int timeout);

    //Get the queue size
    int Size();

private:
    //Release resource interface
    void release();

    //Control the total playback time of data packets in the queue, unit s
    int duration_cur=0;
    int duration_MAX=300;//The maximum cache is 5 minutes, 5*60=300s

    //Multi-thread synchronization settings
    std::mutex mutex_; //Mutex lock, used to prevent resource competition
    std::condition_variable cond_; //pv condition variable, used for thread synchronization

    //Linked list queue, because it is audio and video data, just save the package pointer. Do not save the package to avoid memory explosion.
    std::queue<AVPacket *, std::list<AVPacket *>> queue_;
};

#endif // AVPACKETQUEUE_H

avpacketqueue.cpp

#include "avpacketqueue.h"

//Constructor
AVPacketQueue::AVPacketQueue()
{<!-- -->

}

//destructor
AVPacketQueue::~AVPacketQueue()
{<!-- -->


}

//Enter the queue, put the package into the queue
int AVPacketQueue::Push(AVPacket *val,const int timeout)
{<!-- -->
    //Add scope mutex lock: the resource can only be accessed by one thread at the same time, and the lock will be automatically released when the scope ends.
    std::lock_guard<std::mutex> lock(mutex_);
    //Because it is a packet queue-double linked list
    //If the queue is full, wait for the timeout time
    if(duration_cur>=duration_MAX) {<!-- -->
        // Wait for pop or timeout to wake up
        cond_.wait_for(lock, std::chrono::milliseconds(timeout), [this] {<!-- -->
            return duration_cur<duration_MAX;
        });
    }

    //If the queue is still full, just return -1 directly
    if(duration_cur>=duration_MAX){<!-- -->
        return -1;
    }

    //Enter the queue
    queue_.push(val);
    //Increase queue cache time
    this->duration_cur + =val->duration;

    cond_.notify_one();//Condition variable: notify other threads that they can continue execution
    return 0;
}

//Dequeue, dequeue the package, get the package
AVPacket *AVPacketQueue::Pop(const int timeout)
{<!-- -->
    std::lock_guard<std::mutex> lock(mutex_); //Scope mutex lock
    //If empty, wait for timeout time
    if(queue_.empty()) {<!-- -->
        // Wait for push or timeout to wake up
        cond_.wait_for(lock, std::chrono::milliseconds(timeout), [this] {<!-- -->
            return !queue_.empty() || abort_;
        });
    }
    if(queue_.empty()){<!-- -->
        return nullptr;
    }

    //dequeue
    AVPacket * val = queue_.pop();
    //Reduce queue cache time
    this->duration_cur-=val->duration;

    //Get the value of the last node
    return val;

}