这里给大家分享一个大厂面试的coding题目:实现一个阻塞式的循环队列,具有以下基本功能:
- 队列满时调用write接口会阻塞
- 队列空时调用read接口会阻塞
- 需要保证多线程安全
类的接口声明如下:
template <typename T>
class BlockingCircularBuffer {
public:
explicit BlockingCircularBuffer(int size);
void write(const T& value);
void read(T& value);
bool full();
bool empty();
int capacity();
};
这个题目经过分析后,可以总结为三个关键点:
- 循环队列
- 阻塞等待
- 线程安全
00 循环队列
循环队列通常使用一个固定大小的数组来实现,同时需要两个指针来指示队头和队尾的位置。另外需要识别出队列空和队列满的状态。
用read_index表示在数组中可读位置的索引,用write_index表示在数组中可以写位置的索引。
为了表示出队列空和队列满,这里有两个方法:
- 将read_index == write_index时表示队列空,将(write_index+1)%capacity == read_index时表示队列满,也就是队列最大容量是capacity-1。
- 将read_index替换为data_count,data_count表示当前队列中的有效元素数量。当data_count == 0时表示队列空,当data_count==capacity时表示队列满。那么read_index就需要用write_index-data_count计算得到。
本文后续使用第二种方法。
01 阻塞等待
要达到从空队列读阻塞和向满队列写阻塞,可以考虑c++11提供的条件变量std::condition_variable。以从空队列读阻塞为例,在读数据之前检查队列是否为空,如果为空就阻塞:
std::unique_lock<std::mutex> lg(mutex_);
read_cond_.wait(lg, [this]() { return data_count_ > 0; });
这里的read_cond_就是一个条件变量,它有一个wait方法,调用wait方法后,如果后面的lambda表达式返回false,就会先解锁lg持有锁mutex_,然后阻塞等待,直到有其他线程修改data_count,使得lambda表达式返回true,并且该线程调用条件变量的notify接口。
写线程需要如下代码来取消阻塞读操作:
read_cond_.notify_one();
02 线程安全
所谓线程安全,就是多个线程并发调用同一个队列中的接口时,所有调用都能按预期运行。我们需要对所有在并发访问过程中,可能被修改的数据都进行锁保护。因此,需要被保护的变量有:
- data_count,读写数据读会被修改;
- write_index,写数据时会被修改;
- 数组里每个索引位置里的数据,写数据时会被修改;
数组的大小是在构造过程中就确定的,后面不会再被修改,因此和数组大小相关的访问是不需要进行锁保护的。
03 完整代码
阻塞式循环队列的完整代码如下:
template <typename T>
class BlockingCircularQueue {
public:
explicit BlockingCircularQueue(int size) {
vec_.resize(size);
writer_index_ = 0;
data_count_ = 0;
}
void write(const T& value) {
{
std::unique_lock<std::mutex> lg(mutex_);
write_cond_.wait(lg, [this]() { return data_count_ < vec_.size(); });
vec_[writer_index_] = std::move(value);
writer_index_ = Index(writer_index_ + 1);
++data_count_;
}
read_cond_.notify_one();
}
void read(T& value) {
{
std::unique_lock<std::mutex> lg(mutex_);
read_cond_.wait(lg, [this]() { return data_count_ > 0; });
int reader_index = Index(writer_index_ - data_count_);
--data_count_;
value = std::move(vec_[reader_index]);
}
write_cond_.notify_one();
}
bool full() {
std::lock_guard<std::mutex> lg(mutex_);
return data_count_ == vec_.size();
}
bool empty() {
std::lock_guard<std::mutex> lg(mutex_);
return data_count_ == 0;
}
int capacity() {
return vec_.size();
}
private:
int Index(int i) {
return (i + vec_.size()) % vec_.size();
}
std::mutex mutex_;
std::condition_variable write_cond_;
std::condition_variable read_cond_;
std::vector<T> vec_;
int writer_index_;
int data_count_;
};
04 小结
本文介绍了一个阻塞式循环队列的实现方法,并详细分析了其中三个关键问题:循环队列、阻塞等待、线程安全。
对于阻塞式循环队列,除了本文的方法,还可以考虑用无锁编程实现,当然逻辑上会更复杂一些。
文章目的在于,记录个人最近的收获,如果有读者感兴趣,那刚好可以共同学习与成长。欢迎关注公众号“程序员小阳”,相互沟通软件开发心得。