我的无互斥锁C++生产者消费者代码是否存在死锁或竞态条件?
你的无锁生产者消费者实现的问题分析
嘿,我来帮你拆解这段代码的问题,先直接回答你的核心疑问:
- 死锁?不存在的。死锁的前提是多个线程互相攥着对方需要的同步资源(比如互斥锁)不放,你的代码根本没用到这类同步原语,所以完全不会触发死锁。
- 竞态条件?肯定有,而且在单生产者单消费者场景下有潜在的逻辑瑕疵,要是换成多生产者/多消费者,直接就会出数据错误。
接下来详细说问题:
1. 非原子的unread计算藏着坑
在waitForReaderToCatchUp和waitForWriterToCatchUp里,unread = writePos - readPos这一步看着没问题,但其实有竞态:
虽然writePos和readPos都是原子变量,但读取两个原子然后做减法的整个过程不是原子操作。举个实际场景:
- 生产者先读了
writePos = 5,这时候消费者刚好把readPos从0改成了1 - 生产者再读
readPos = 1,算出unread =4,结果是对的,但反过来: - 消费者先读
readPos=0,生产者立刻把writePos从4改成5 - 消费者再读
writePos=5,算出unread=5,结果也对?其实在单生产者单消费者的情况下,因为两个变量只会递增,这个误差不会直接导致错误,但会出现不必要的等待延迟——比如明明消费者已经读了数据,生产者却还在傻等。
但如果是多生产者/多消费者,这个问题就炸了:两个生产者同时读writePos,会拿到同一个值,然后往同一个缓冲区位置写,直接覆盖数据;多个消费者也会同时读同一个位置,重复消费。
2. 没限制并发,多线程直接翻车
你的代码现在是单生产者单消费者在跑,但CircularBuffer的设计没做任何限制:
- 要是开多个生产者线程,它们会同时调用
getWritePos,拿到同一个缓冲区位置,写数据的时候直接互相覆盖,数据全乱。 - 要是开多个消费者线程,它们会同时拿到同一个读取位置,重复读同一份数据,消费逻辑直接出错。
3. 等待策略太浪费CPU
你用std::this_thread::sleep_for(std::chrono::nanoseconds(10))来做等待,这本质上是低效的忙等:
10纳秒的睡眠时间极短,线程刚睡就被唤醒,然后又去检查条件,大部分时间都在空转,会吃掉大量CPU资源。更高效的做法是用条件变量配合互斥锁,或者如果坚持无锁,用自旋等待(但只适合短时间等待)。
4. 代码里的小细节可以优化
memcpy(p, &i, 4);完全可以写成*p = i;,手动写4字节依赖于int是4字节,这在某些平台上可能不成立,直接赋值既安全又好读。rand()不是线程安全的,要是以后扩展多线程用它,会生成错误的随机数,建议换成C++11的<random>库的线程安全随机数工具。
单生产者单消费者无锁缓冲区的正确姿势
如果要做真正安全的单生产者单消费者无锁环形缓冲区,得注意这几点:
- 严格保证生产者只碰
writePos,消费者只碰readPos,两者操作完全独立。 - 计算可用空间/数据量时,别用
writePos - readPos,因为整数溢出后结果会错,应该用环形缓冲区的模运算逻辑来判断。 - 用对内存顺序,单生产者单消费者场景下,
writePos和readPos的读写可以用std::memory_order_acquire和std::memory_order_release,减少不必要的内存屏障,提升性能。
内容的提问来源于stack exchange,提问作者Andy
相关产品推荐
相关产品推荐

