网站新闻字体免费代理上网网站
C++并发编程:构建线程安全队列(第二部分:细粒度锁)
1. 引言
在多线程环境下,为了保证数据的一致性和正确性,需要使用同步原语来对共享数据的访问进行互斥和同步。std::queue
作为一种先进先出(FIFO)的数据结构,它本身并不是线程安全的,同时访问它可能导致数据竞争和不一致的问题。
所以在多线程环境下,我们需要构建一个线程安全的队列,使其可以安全地被多个线程同时访问,而不会出现数据竞争。这里我们使用C++11中的一些新特性如std::shared_ptr
、std::unique_ptr
、std::mutex
、std::condition_variable
等来实现一个线程安全队列。
2. 设计要点
线程安全队列的关键设计要点包括:
- 使用互斥量保护队列的头尾指针,使多线程不能同时修改队列结构
- 使用条件变量实现线程之间的通知和等待,避免忙等待(busy waiting)
- 提供多种不同的访问接口如阻塞、非阻塞、等待获取等,增强队列的实用性
- 使用
std::shared_ptr
、std::unique_ptr
等智能指针管理内存,避免手动new/delete带来的问题 - 尽量减少锁的粒度,例如头尾指针使用不同的锁,以提高并发性
下面我们来看一下这个线程安全队列的具体设计和实现。
3. 队列的节点结构
队列节点node
使用一个std::shared_ptr
保存数据,和一个std::unique_ptr
指向下一节点:
struct node
{std::shared_ptr<T> data;std::unique_ptr<node> next;
};
使用shared_ptr
而不是原始指针管理数据,可以自动释放内存,避免泄漏。unique_ptr
则保证节点间的引用关系唯一,方便传递所有权。
4. 队列结构
队列结构包含头尾指针、互斥量和条件变量:
std::mutex head_mutex;
std::unique_ptr<node> head;std::mutex tail_mutex;
node* tail;std::condition_variable data_cond;
头指针使用unique_ptr
且初始化为一个哑节点。尾指针使用原始指针,初始化指向哑节点。
头尾指针分别使用head_mutex
和tail_mutex
保护,以减小锁的粒度,不同指针可以并发修改。
条件变量data_cond
用于在队列为空时等待,并在有数据可获取时发出通知。
5. 主要接口
队列主要提供下面一些接口:
5.1 非阻塞获取
try_pop
尝试非阻塞地弹出队头元素,如果队列为空则直接返回:
std::shared_ptr<T> try_pop()
{std::lock_guard<std::mutex> head_lock(head_mutex);if (head.get() == get_tail()) {return {}; }return std::move(head->data);
}
仅对头指针上锁,执行常数时间操作,可并发访问。
5.2 阻塞获取
wait_and_pop
会在队列为空时等待,直到有元素入队后唤醒线程并返回数据:
std::shared_ptr<T> wait_and_pop()
{std::unique_lock<std::mutex> head_lock(wait_for_data());return std::move(head->data);
}std::unique_lock<std::mutex> wait_for_data()
{std::unique_lock<std::mutex>