大厂面试coding--阻塞式循环队列

这里给大家分享一个大厂面试的coding题目:实现一个阻塞式的循环队列,具有以下基本功能:

  1. 队列满时调用write接口会阻塞
  2. 队列空时调用read接口会阻塞
  3. 需要保证多线程安全

类的接口声明如下:

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();
};

这个题目经过分析后,可以总结为三个关键点:

  1. 循环队列
  2. 阻塞等待
  3. 线程安全

00 循环队列

循环队列通常使用一个固定大小的数组来实现,同时需要两个指针来指示队头和队尾的位置。另外需要识别出队列空和队列满的状态。
在这里插入图片描述
用read_index表示在数组中可读位置的索引,用write_index表示在数组中可以写位置的索引。

为了表示出队列空和队列满,这里有两个方法:

  1. 将read_index == write_index时表示队列空,将(write_index+1)%capacity == read_index时表示队列满,也就是队列最大容量是capacity-1。
  2. 将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 线程安全

所谓线程安全,就是多个线程并发调用同一个队列中的接口时,所有调用都能按预期运行。我们需要对所有在并发访问过程中,可能被修改的数据都进行锁保护。因此,需要被保护的变量有:

  1. data_count,读写数据读会被修改;
  2. write_index,写数据时会被修改;
  3. 数组里每个索引位置里的数据,写数据时会被修改;

数组的大小是在构造过程中就确定的,后面不会再被修改,因此和数组大小相关的访问是不需要进行锁保护的。

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 小结

本文介绍了一个阻塞式循环队列的实现方法,并详细分析了其中三个关键问题:循环队列、阻塞等待、线程安全。
对于阻塞式循环队列,除了本文的方法,还可以考虑用无锁编程实现,当然逻辑上会更复杂一些。

文章目的在于,记录个人最近的收获,如果有读者感兴趣,那刚好可以共同学习与成长。欢迎关注公众号“程序员小阳”,相互沟通软件开发心得。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值