C++ 线程安全栈数据结构示例
在多线程编程中,确保数据结构线程安全非常重要。下面是一个简单的线程安全栈数据结构的实现示例,并详细说明设计要点和容易忽略的安全点。
#include <iostream>
#include <stack>
#include <mutex>
#include <thread>
#include <condition_variable>
template <typename T>
class ThreadSafeStack {
private:
std::stack<T> data;
mutable std::mutex mtx; // 互斥锁,用于保护数据
std::condition_variable cond; // 条件变量,用于线程同步
public:
// 构造函数
ThreadSafeStack() : data(), mtx(), cond() {}
// 禁用拷贝构造函数和赋值操作符
ThreadSafeStack(const ThreadSafeStack&) = delete;
ThreadSafeStack& operator=(const ThreadSafeStack&) = delete;
// 入栈操作
void push(T value) {
std::lock_guard<std::mutex> lock(mtx); // 加锁
data.push(value);
cond.notify_one(); // 通知等待的线程
}
// 出栈操作,如果栈为空则等待
void pop(T& value) {
std::unique_lock<std::mutex> lock(mtx); // 加锁
cond.wait(lock, [this] { return !data.empty(); }); // 等待直到栈不为空
value = data.top();
data.pop();
}
// 尝试出栈操作,如果栈为空则返回false
bool try_pop(T& value) {
std::lock_guard<std::mutex> lock(mtx); // 加锁
if (data.empty()) {
return false;
}
value = data.top();
data.pop();
return true;
}
// 检查栈是否为空
bool empty() const {
std::lock_guard<std::mutex> lock(mtx); // 加锁
return data.empty();
}
// 返回栈的大小
size_t size() const {
std::lock_guard<std::mutex> lock(mtx); // 加锁
return data.size();
}
};
// 示例使用
void push_data(ThreadSafeStack<int>& stack, int start, int end) {
for (int i = start; i < end; ++i) {
stack.push(i);
}
}
void pop_data(ThreadSafeStack<int>& stack, int count) {
for (int i = 0; i < count; ++i) {
int value;
stack.pop(value);
std::cout << "Popped: " << value << std::endl;
}
}
int main() {
ThreadSafeStack<int> stack;
std::thread t1(push_data, std::ref(stack), 0, 10);
std::thread t2(push_data, std::ref(stack), 10, 20);
std::thread t3(pop_data, std::ref(stack), 10);
std::thread t4(pop_data, std::ref(stack), 10);
t1.join();
t2.join();
t3.join();
t4.join();
return 0;
}
设计要点
互斥锁(Mutex):使用std::mutex来保护对共享数据的所有访问。确保在访问或修改栈数据时,始终持有互斥锁。
条件变量(Condition Variable):使用std::condition_variable来同步线程。在pop操作中,如果栈为空,线程会等待,直到有数据入栈。这样可以避免忙等待,节省CPU资源。
拷贝构造函数和赋值操作符:为了避免竞态条件和不必要的复杂性,禁用了拷贝构造函数和赋值操作符。
线程安全操作:所有对栈数据的操作(如push、pop、try_pop、empty、size)都在互斥锁的保护下进行,确保线程安全。
容易忽略的安全点
条件变量的误用:在使用条件变量时,必须确保在调用wait之前已经持有互斥锁,并且在notify之前释放互斥锁。否则,可能会导致线程死锁或错过通知。
锁的粒度:在某些情况下,锁的粒度可能会影响性能。如果锁的粒度过大(例如在每个操作中都持有锁),可能会导致性能瓶颈。在设计时,需要权衡锁的粒度和并发性能。
异常安全:在多线程环境中,异常处理变得更加复杂。确保在抛出异常时,锁能够正确释放,避免死锁。
内存模型:在多线程环境中,内存模型的选择会影响线程之间的可见性和顺序性。确保在必要时使用std::atomic或std::memory_order来控制内存顺序。
通过以上设计要点和注意事项,可以实现一个简单且线程安全的栈数据结构,确保在多线程环境下的正确性和性能。
std::unique_lock<std::mutex>和std::lock_guard<std::mutex>应用说明
在C++中,std::unique_lock和std::lock_guard都是用于管理互斥锁的RAII(Resource Acquisition Is Initialization)类。它们的主要区别在于灵活性和功能性。下面详细说明它们的应用场景和区别。
std::lock_guard
特点
简单:std::lock_guard是一个轻量级的锁管理工具,使用非常简单。
固定作用域:std::lock_guard的生命周期与其所在的代码块(通常是函数或循环)一致,当作用域结束时,锁自动释放。
不可移动:std::lock_guard不支持移动语义,因此不能转移到另一个作用域。
应用场景
简单的锁管理:当只需要在特定作用域内锁定和解锁互斥锁时,使用std::lock_guard非常方便。
自动释放锁:确保在作用域结束时自动释放锁,避免忘记解锁导致的死锁。
示例
void some_function(std::mutex& mtx) {
std::lock_guard<std::mutex> lock(mtx); // 锁定互斥锁
// 执行需要保护的操作
} // 作用域结束,自动释放锁
std::unique_lock
特点
灵活:std::unique_lock提供了更高级的功能,包括延迟锁定、手动解锁、转移所有权等。
可移动:std::unique_lock支持移动语义,可以转移到另一个作用域。
条件变量配合:std::unique_lock与std::condition_variable一起使用时,可以支持wait和notify操作。
应用场景
延迟锁定:当需要延迟锁定互斥锁时,例如在某些条件满足时才锁定。
手动解锁:在某些情况下,需要在作用域结束前手动解锁互斥锁。
条件变量:与std::condition_variable一起使用,实现复杂的线程同步。
锁的转移:当需要将锁的所有权从一个线程转移到另一个线程时。
示例
void some_function(std::mutex& mtx, std::condition_variable& cond, bool& ready) {
std::unique_lock<std::mutex> lock(mtx); // 锁定互斥锁
// 等待某个条件变量
cond.wait(lock, [&ready]{ return ready; }); // 等待ready为true
// 执行需要保护的操作
lock.unlock(); // 手动解锁
// 其他不需要保护的操作
}
总结
std::lock_guard 适用于简单的锁管理,主要用于在固定的作用域内锁定和解锁互斥锁。
std::unique_lock 适用于更复杂的场景,如延迟锁定、手动解锁、与条件变量配合使用以及锁的转移。
在选择使用哪种锁管理工具时,应根据具体需求和场景来决定。如果只需要简单的锁管理,std::lock_guard是一个很好的选择;如果需要更高级的功能,例如与条件变量配合使用,那么std::unique_lock是更合适的选择。