一、多线程编程的挑战与线程安全队列的诞生
在当今的软件开发领域,多线程编程已经成为提升程序性能和响应速度的关键技术。想象一下,我们的程序就像一个繁忙的工厂,每个线程都是工厂里的工人,它们可以同时执行不同的任务,大大提高了生产效率。然而,就像工厂里的工人共享工具和原材料一样,多个线程也会共享程序中的数据。这就带来了一个严重的问题 —— 数据竞争。
数据竞争是指多个线程同时访问和修改共享数据,而没有进行适当的同步控制,这可能会导致数据的不一致性和程序的不稳定。例如,一个线程正在读取某个数据,而另一个线程同时在修改这个数据,那么读取到的数据可能是不完整或错误的。为了解决这个问题,我们需要一种机制来确保在同一时间只有一个线程可以访问和修改共享数据,这就是线程安全的概念。
线程安全队列就是为了解决多线程环境下数据共享问题而设计的一种数据结构。它就像工厂里的一个有序仓库,多个工人(线程)可以安全地将货物(数据)存入或取出,而不会发生混乱。
二、线程安全队列的核心组件与原理
(一)互斥锁(std::mutex)
互斥锁是线程安全队列的核心组件之一,它就像仓库的一把锁,同一时间只允许一个工人进入仓库操作货物。在 C++ 中,std::mutex
是标准库提供的互斥锁实现。当一个线程想要访问共享数据时,它需要先获取这把锁,如果锁已经被其他线程持有,那么这个线程就会被阻塞,直到锁被释放。
(二)条件变量(std::condition_variable)
条件变量是另一个重要的组件,它就像仓库的一个广播系统。当仓库为空时,想要取货的工人(线程)可以通过条件变量进入等待状态,当有新的货物存入仓库时,存入货物的工人(线程)可以通过条件变量广播通知等待的工人。在 C++ 中,std::condition_variable
可以与 std::mutex
配合使用,实现线程间的同步。
(三)队列(std::queue)
队列是线程安全队列的底层存储结构,它遵循先进先出(FIFO)的原则,就像仓库里的货物按照存入的顺序依次取出。在 C++ 中,std::queue
是标准库提供的队列实现。
三、示例代码详解
(一)线程安全队列类的定义
template <typename T>
class threadsafe_queue {
private:
mutable std::mutex mut;
std::queue<T> data_queue;
std::condition_variable data_cond;
template <typename T>
:这是一个模板类的声明,意味着这个队列可以存储任意类型的数据,T
是一个类型参数。mutable std::mutex mut
:mutable
关键字允许在const
成员函数中修改mut
。std::mutex
用于保护对data_queue
的访问,确保同一时间只有一个线程可以操作队列。std::queue<T> data_queue
:这是实际存储数据的队列,使用标准库的std::queue
实现。std::condition_variable data_cond
:条件变量,用于线程间的同步,当队列有新元素或为空时通知等待的线程。
(二)构造函数与拷贝构造函数
public:
threadsafe_queue() {}
threadsafe_queue(const threadsafe_queue& other) {
std::lock_guard<std::mutex> lk(other.mut);
data_queue = other.data_queue;
}
threadsafe_queue()
:默认构造函数,用于初始化队列。threadsafe_queue(const threadsafe_queue& other)
:拷贝构造函数,用于创建一个新的队列,其内容与另一个队列相同。在拷贝过程中,使用std::lock_guard
锁定原队列的互斥锁,确保在拷贝过程中不会有其他线程修改原队列。
(三)入队操作(push)
void push(T new_value) {
std::lock_guard<std::mutex> lk(mut);
data_queue.push(new_value);
data_cond.notify_one();
}
std::lock_guard<std::mutex> lk(mut)
:使用std::lock_guard
自动管理互斥锁的生命周期。当lk
对象创建时,它会自动锁定mut
;当lk
对象销毁时,它会自动释放mut
。data_queue.push(new_value)
:将新元素插入到队列的尾部。data_cond.notify_one()
:通知一个等待在data_cond
上的线程,告诉它队列中有新元素了。
(四)出队操作(wait_and_pop)
void wait_and_pop(T& value) {
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk, [this] { return!data_queue.empty(); });
value = data_queue.front();
data_queue.pop();
}
std::shared_ptr<T> wait_and_pop() {
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk, [this] { return!data_queue.empty(); });
std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
data_queue.pop();
return res;
}
std::unique_lock<std::mutex> lk(mut)
:std::unique_lock
比std::lock_guard
更灵活,它允许在需要时手动释放和重新获取锁。data_cond.wait(lk, [this] { return!data_queue.empty(); })
:这是一个关键的同步操作。如果队列为空,线程会释放mut
并进入等待状态;当有新元素插入队列时,data_cond.notify_one()
会唤醒一个等待的线程,线程会重新获取mut
并检查队列是否为空。如果队列仍然为空,线程会继续等待;如果队列不为空,线程会继续执行后续代码。value = data_queue.front()
和std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()))
:从队列的头部取出元素。data_queue.pop()
:从队列中移除头部元素。
(五)尝试出队操作(try_pop)
bool try_pop(T& value) {
std::lock_guard<std::mutex> lk(mut);
if (data_queue.empty())
return false;
value = data_queue.front();
data_queue.pop();
return true;
}
std::shared_ptr<T> try_pop() {
std::lock_guard<std::mutex> lk(mut);
if (data_queue.empty())
return std::shared_ptr<T>();
std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
data_queue.pop();
return res;
}
try_pop
函数尝试从队列中取出元素,但不会阻塞线程。如果队列为空,函数会立即返回false
或空的std::shared_ptr
;如果队列不为空,函数会取出元素并返回true
或元素的std::shared_ptr
。
(六)检查队列是否为空(empty)
bool empty() const {
std::lock_guard<std::mutex> lk(mut);
return data_queue.empty();
}
empty
函数用于检查队列是否为空。在检查过程中,使用std::lock_guard
锁定互斥锁,确保在检查过程中不会有其他线程修改队列。
四、测试代码分析
(一)生产者函数(producer)
// 测试函数,向线程安全队列中插入数据
void producer(threadsafe_queue<int>& q, int num) {
for (int i = 0; i < num; ++i) {
q.push(i);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
producer
函数模拟了生产者线程的行为。它会循环num
次,每次将一个整数插入到线程安全队列中,然后休眠 100 毫秒。
(二)消费者函数(consumer)
// 测试函数,从线程安全队列中取出数据
void consumer(threadsafe_queue<int>& q, int num) {
int value;
for (int i = 0; i < num; ++i) {
if (q.try_pop(value)) {
std::cout << "Consumed: " << value << std::endl;
} else {
std::cout << "Queue is empty while consuming" << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(150));
}
}
consumer
函数模拟了消费者线程的行为。它会循环num
次,每次尝试从线程安全队列中取出一个元素。如果成功取出元素,会打印消费信息;如果队列为空,会打印队列空的提示信息,然后休眠 150 毫秒。
五、整体代码和运行测试结果
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <stack>
#include <queue>
#include <condition_variable>
template<typename T>
class threadsafe_queue
{
private:
mutable std::mutex mtx;
std::queue<T> data_queue;
std::condition_variable data_cond;
public:
threadsafe_queue(){}
threadsafe_queue(threadsafe_queue const& other){
std::lock_guard<std::mutex> lock(other.mtx);
data_queue = other.data_queue;
}
void push(T new_value) {
std::lock_guard<std::mutex> lock(mtx);
data_queue.push(new_value);
data_cond.notify_one();
}
void wait_and_pop(T &value){
std::unique_lock<std::mutex> lock(mtx);
data_cond.wait(lock, [this]{return !data_queue.empty();});
value = data_queue.front();
data_queue.pop();
}
std::shared_ptr<T> wait_and_pop(){
std::unique_lock<std::mutex> lock(mtx);
data_cond.wait(lock, [this]{return !data_queue.empty();});
std::shared_ptr<T> value(std::make_shared<T>(data_queue.front()));
data_queue.pop();
return value;
}
bool try_pop(T& value) {
std::lock_guard<std::mutex> lock(mtx);
if (data_queue.empty())
return false;
value = data_queue.front();
data_queue.pop();
return true;
}
std::shared_ptr<T> try_pop(){
std::lock_guard<std::mutex> lock(mtx);
if (data_queue.empty())
return std::shared_ptr<T>();
std::shared_ptr<T> value(std::make_shared<T>(data_queue.front()));
data_queue.pop();
return value;
}
bool empty() const {
std::lock_guard<std::mutex> lock(mtx);
return data_queue.empty();
}
};
// 测试函数,向线程安全队列中插入数据
void producer(threadsafe_queue<int>& q, int num) {
for (int i = 0; i < num; ++i) {
q.push(i);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
// 测试函数,从线程安全队列中取出数据
void consumer(threadsafe_queue<int>& q, int num) {
int value;
for (int i = 0; i < num; ++i) {
if (q.try_pop(value)) {
std::cout << "Consumed: " << value << std::endl;
} else {
std::cout << "Queue is empty while consuming" << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(150));
}
}
int main() {
threadsafe_queue<int> ts_queue;
const int num_elements = 100;
std::vector<std::thread> producers_vec;
std::vector<std::thread> consumers_vec;
// 创建生产者线程
for (int i = 0; i < 33; ++i) {
producers_vec.emplace_back(producer, std::ref(ts_queue), num_elements);
}
// 创建消费者线程
for (int i = 0; i < 22; ++i) {
consumers_vec.emplace_back(consumer, std::ref(ts_queue), num_elements);
}
// 等待生产者线程完成
for (auto& th : producers_vec) {
th.join();
}
// 等待消费者线程完成
for (auto& th : consumers_vec) {
th.join();
}
return 0;
}
- 创建
threadsafe_queue<int>
实例ts_queue
用于测试。 - 定义要生产和消费的元素数量
num_elements
。 - 创建三个生产者线程和两个消费者线程,分别放入
producers_vec
和consumers_vec
向量中。 - 使用
join
方法等待所有生产者和消费者线程执行完毕,确保程序在所有线程完成后退出。
运行结果
Consumed: 0
Consumed: 0
Consumed: 0
Consumed: 0
Consumed: 0
Consumed: 0
Consumed: 0
Consumed: 0
Consumed: 1
Consumed: 0
Consumed: 1
Consumed: 1
Consumed: 1
Consumed: 1
Consumed: 1
通过以上步骤,代码实现了一个线程安全的队列,并通过多线程测试了队列的入队和出队操作。