BoundedBuffer 之我见

本文通过BoundedBuffer实例解析了Java并发包中Condition的使用方法。深入探讨了notFull和notEmpty两个Condition对象如何实现线程间的同步等待与唤醒机制。

看到java.util.concurrent.locks.*的Condition里面给出了一个例子,是BoundedBuffer,这个例子倒是不是很难,可是说是很经典的一个应用。

class BoundedBuffer {
		final Lock lock = new ReentrantLock();
		final Condition notEmpty = lock.newCondition();
		final Condition notFull = lock.newCondition();

		final int MAX_SIZE = 5;
		List<Object> list = new ArrayList<Object>();
		int count = 0;

		public void put(Object obj) throws InterruptedException {
			lock.lock();
			try {
				while (count == MAX_SIZE) {
					notFull.await();
				}
				list.add(obj);
				count++;
				System.out.println("call: notEmpty.signal()");
				notEmpty.signal();
			} finally {
				lock.unlock();
			}
		}

		public Object take() throws InterruptedException {
			lock.lock();
			try {
				while (count == 0) {
					notEmpty.await();
				}
				Object obj = list.remove(0);
				count --;
				System.out.println("call: notFull.signal()");
				notFull.signal();
				return obj;
			} finally {
				lock.unlock();
			}
		}
	}


对于Java变量的命名,我相信Sun公司的人一般不会选择没有含义的名称,可是在这里 一个notFull和notEmpty让我很是纠结notFull.await()是什么意识?notEmpty.await()以及相应的signal()是什么意思,当然代码是最好的倒是,但要是从意义上来理解就更好了。

不难看出,notEmpty表示的是缓存非空,那么notEmpty.await()表示的意识就是缓存非空这个条件为假(同意是‘现在缓存是空的!’所以,老大给我停下来),相应的notEmpty.signal() 就表示缓存非空为真,然后就祈祷:神啊!(在这里的神当然指的是系统调度了)我已经准备好了,可以开始了。同理也能够理解notFull了。


<think>我们正在设计一个线程安全的有界缓冲区模板类BoundedBuffer。它应该是一个固定大小的循环队列,支持多线程环境下的安全读写。我们将使用互斥锁(mutex)和条件变量(condition variable)来实现线程同步。 设计要点: 1. 使用循环队列(环形缓冲区)作为底层数据结构,用数组或vector存储数据。 2. 维护两个索引:一个用于写入(push),一个用于读取(pop)。 3. 使用互斥锁保护共享数据(索引和底层数据结构)。 4. 使用两个条件变量:一个用于等待缓冲区非空(消费者等待),一个用于等待缓冲区非满(生产者等待)。 成员变量: T* buffer; // 或者使用std::vector<T>,存储数据的数组 size_t capacity; // 缓冲区的容量 size_t front; // 指向队列头,下一个被读取的位置 size_t back; // 指向队列尾,下一个被写入的位置 size_t count; // 当前缓冲区中的元素个数(也可以使用front和back计算,但用count更清晰) std::mutex mtx; // 互斥锁 std::condition_variable not_full; // 当缓冲区满时,生产者等待在此条件变量上 std::condition_variable not_empty; // 当缓冲区空时,消费者等待在此条件变量上 注意:循环队列的实现,我们通常使用模运算来移动索引。 成员函数: void push(const T& item); // 将元素放入缓冲区(如果缓冲区满则阻塞) void push(T&& item); // 移动版本 bool try_push(const T& item); // 非阻塞版本,如果缓冲区满则返回false void pop(T& item); // 从缓冲区取出元素(如果缓冲区空则阻塞) bool try_pop(T& item); // 非阻塞版本,如果缓冲区空则返回false // 还可以考虑添加一些辅助函数,如size(), empty(), full()等,但要注意线程安全 实现细节: push操作: 1. 获取互斥锁(使用std::unique_lock) 2. 检查缓冲区是否已满(count == capacity),如果满则等待在not_full条件变量上(使用条件变量的wait函数,并传入互斥锁和判断条件) 3. 将元素放入back位置,然后更新back和count 4. 通知消费者(not_empty.notify_one()) 5. 解锁(在unique_lock离开作用域时自动解锁) pop操作类似。 注意:条件变量的wait函数需要一个谓词(predicate)来避免虚假唤醒。我们可以使用lambda表达式来检查条件。 另外,我们还需要考虑异常安全。如果T的复制或移动可能抛出异常,我们需要确保在发生异常时,锁和条件变量状态仍然正确。由于我们使用RAII(unique_lock),所以锁会在异常时自动释放。 下面我们开始实现这个模板类。 注意:我们使用std::vector作为底层容器,也可以使用动态数组,但要注意管理内存。这里我们使用vector,并在构造函数中分配空间。 另外,我们也可以使用标准库的std::queue作为底层容器,但需要额外处理同步。但循环队列使用数组实现更直观。 我们选择使用固定大小的vector,并在构造时指定大小。 实现:</think>### 线程安全有界缓冲区模板类实现 #### 设计思路 设计一个线程安全的有界缓冲区模板类 `BoundedBuffer`,需要满足: 1. 固定容量(有界) 2. 线程安全的读写操作 3. 支持阻塞操作(缓冲区满时阻塞生产者,空时阻塞消费者) 4. 使用循环队列提高空间利用率 5. 使用互斥锁保护共享数据 6. 使用条件变量实现线程同步 #### 完整实现代码 ```cpp #include <vector> #include <mutex> #include <condition_variable> template <typename T> class BoundedBuffer { public: explicit BoundedBuffer(size_t capacity) : capacity_(capacity), buffer_(capacity), front_(0), rear_(0), count_(0) {} // 阻塞式写入 void push(const T& item) { std::unique_lock<std::mutex> lock(mutex_); not_full_.wait(lock, [this] { return count_ < capacity_; }); buffer_[rear_] = item; rear_ = (rear_ + 1) % capacity_; ++count_; not_empty_.notify_one(); } // 阻塞式读取 T pop() { std::unique_lock<std::mutex> lock(mutex_); not_empty_.wait(lock, [this] { return count_ > 0; }); T item = buffer_[front_]; front_ = (front_ + 1) % capacity_; --count_; not_full_.notify_one(); return item; } // 非阻塞尝试写入 bool try_push(const T& item) { std::unique_lock<std::mutex> lock(mutex_); if (count_ == capacity_) return false; buffer_[rear_] = item; rear_ = (rear_ + 1) % capacity_; ++count_; not_empty_.notify_one(); return true; } // 非阻塞尝试读取 bool try_pop(T& item) { std::unique_lock<std::mutex> lock(mutex_); if (count_ == 0) return false; item = buffer_[front_]; front_ = (front_ + 1) % capacity_; --count_; not_full_.notify_one(); return true; } size_t size() const { std::lock_guard<std::mutex> lock(mutex_); return count_; } bool empty() const { std::lock_guard<std::mutex> lock(mutex_); return count_ == 0; } bool full() const { std::lock_guard<std::mutex> lock(mutex_); return count_ == capacity_; } private: const size_t capacity_; std::vector<T> buffer_; size_t front_; // 读取位置 size_t rear_; // 写入位置 size_t count_; // 当前元素数量 mutable std::mutex mutex_; std::condition_variable not_full_; // 缓冲区不满条件 std::condition_variable not_empty_; // 缓冲区不空条件 }; ``` #### 关键实现解析 1. **循环队列设计** - 使用 `front_` 和 `rear_` 指针实现环形缓冲区 - 通过取模运算实现循环:`(index + 1) % capacity_` - 提高空间利用率,避免频繁内存分配 2. **线程同步机制** - **互斥锁 (`mutex_`)**:保护共享数据(`front_`, `rear_`, `count_`, `buffer_`) - **条件变量 (`not_full_`)**:生产者等待缓冲区不满 - **条件变量 (`not_empty_`)**:消费者等待缓冲区不空 - 使用 `std::unique_lock` 配合条件变量,支持条件等待时的锁释放 3. **阻塞操作实现** ```cpp not_full_.wait(lock, [this] { return count_ < capacity_; }); ``` - 条件变量等待时自动释放锁,避免忙等待 - 使用谓词防止虚假唤醒(spurious wakeup) 4. **通知机制** - 写入后通知消费者:`not_empty_.notify_one()` - 读取后通知生产者:`not_full_.notify_one()` ### 生产者-消费者示例 ```cpp #include <iostream> #include <thread> BoundedBuffer<int> buffer(5); // 容量为5的缓冲区 void producer() { for (int i = 0; i < 10; ++i) { buffer.push(i); std::cout << "Produced: " << i << std::endl; } } void consumer() { for (int i = 0; i < 10; ++i) { int item = buffer.pop(); std::cout << "Consumed: " << item << std::endl; } } int main() { std::thread prod_thread(producer); std::thread cons_thread(consumer); prod_thread.join(); cons_thread.join(); return 0; } ``` ### C++面试问题准备 #### 基础概念 1. **什么是有界缓冲区?为什么需要线程安全实现?** 有界缓冲区是固定容量的共享数据结构,用于生产者和消费者之间的数据传递。需要线程安全保证多线程并发访问的正确性[^2]。 2. **解释生产者-消费者问题及其同步挑战** 生产者生成数据放入缓冲区,消费者从缓冲区取出数据。主要挑战是: - 缓冲区满时生产者等待 - 缓冲区空时消费者等待 - 避免竞态条件(race condition) 3. **互斥锁与条件变量的区别?** - 互斥锁:保护共享数据的互斥访问 - 条件变量:用于线程间状态通知,必须与互斥锁配合使用 #### 实现细节 4. **为什么使用循环队列而不是普通队列?** 循环队列(环形缓冲区): - 空间利用率高($O(1)$ 空间复杂度) - 避免频繁内存分配 - 适合固定大小的缓冲区场景 5. **如何处理虚假唤醒(spurious wakeup)?** 使用带有谓词的条件变量等待: ```cpp not_full_.wait(lock, [this] { return count_ < capacity_; }); ``` 即使虚假唤醒,也会重新检查条件 6. **`notify_one()` 和 `notify_all()` 如何选择?** - `notify_one()`:唤醒一个等待线程(效率高) - `notify_all()`:唤醒所有等待线程(适用于多个等待线程可同时执行) #### 高级话题 7. **如何扩展支持超时操作?** 使用条件变量的 `wait_for` 或 `wait_until`: ```cpp bool try_push_for(const T& item, std::chrono::milliseconds timeout) { std::unique_lock<std::mutex> lock(mutex_); if (!not_full_.wait_for(lock, timeout, [this] { return count_ < capacity_; })) { return false; // 超时 } // ... 写入操作 return true; } ``` 8. **无锁实现与有锁实现的优缺点对比?** - **有锁实现**: - ✅ 实现简单 - ✅ 通用性强 - ❌ 可能有锁竞争开销 - **无锁实现**: - ✅ 无锁竞争开销 - ✅ 高并发性能好 - ❌ 实现复杂(需处理ABA问题) - ❌ 内存管理复杂[^1] 9. **如何测试线程安全容器的正确性?** - 多线程压力测试(高并发读写) - 验证最终一致性 - 使用线程检查工具(如TSan) - 边界条件测试(空、满、单元素) #### 设计模式 10. **BoundedBuffer 在哪些场景适用?** 适用于需要流量控制的生产者-消费者场景: - 网络数据包缓冲 - 日志批量写入 - 任务调度队列 - 实时数据处理管道
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值